JWTCreatorTest.java

package com.auth0.jwt;

import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.ECDSAKeyProvider;
import com.auth0.jwt.interfaces.RSAKeyProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import java.nio.charset.StandardCharsets;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.RSAPrivateKey;
import java.time.Instant;
import java.util.*;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class JWTCreatorTest {

    private static final String PRIVATE_KEY_FILE_RSA = "src/test/resources/rsa-private.pem";
    private static final String PRIVATE_KEY_FILE_EC_256 = "src/test/resources/ec256-key-private.pem";

    @Rule
    public ExpectedException exception = ExpectedException.none();

    @Test
    public void shouldThrowWhenRequestingSignWithoutAlgorithm() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("The Algorithm cannot be null");
        JWTCreator.init()
                .sign(null);
    }

    @Test
    public void shouldAddHeaderClaim() {
        Date date = new Date(123000);
        Instant instant = date.toInstant();

        List<Object> list = Arrays.asList(date, instant);
        Map<String, Object> map = new HashMap<>();
        map.put("date", date);
        map.put("instant", instant);

        List<Object> expectedSerializedList = Arrays.asList(date.getTime() / 1000, instant.getEpochSecond());
        Map<String, Object> expectedSerializedMap = new HashMap<>();
        expectedSerializedMap.put("date", date.getTime() / 1000);
        expectedSerializedMap.put("instant", instant.getEpochSecond());

        Map<String, Object> header = new HashMap<>();
        header.put("string", "string");
        header.put("int", 42);
        header.put("long", 4200000000L);
        header.put("double", 123.123);
        header.put("bool", true);
        header.put("date", date);
        header.put("instant", instant);
        header.put("list", list);
        header.put("map", map);

        String signed = JWTCreator.init()
                .withHeader(header)
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry("string", "string"));
        assertThat(headerJson, JsonMatcher.hasEntry("int", 42));
        assertThat(headerJson, JsonMatcher.hasEntry("long", 4200000000L));
        assertThat(headerJson, JsonMatcher.hasEntry("double", 123.123));
        assertThat(headerJson, JsonMatcher.hasEntry("bool", true));
        assertThat(headerJson, JsonMatcher.hasEntry("date", 123));
        assertThat(headerJson, JsonMatcher.hasEntry("instant", 123));
        assertThat(headerJson, JsonMatcher.hasEntry("list", expectedSerializedList));
        assertThat(headerJson, JsonMatcher.hasEntry("map", expectedSerializedMap));
    }

    @Test
    public void shouldReturnBuilderIfNullMapIsProvided() {
        Map<String, Object> nullMap = null;
        String nullString = null;
        String signed = JWTCreator.init()
                .withHeader(nullMap)
                .withHeader(nullString)
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
    }

    @Test
    public void shouldSupportJsonValueHeaderWithNestedDataStructure() {
        String stringClaim = "someClaim";
        Integer intClaim = 1;
        List<String> nestedListClaims = Arrays.asList("1", "2");
        String claimsJson = "{\"stringClaim\": \"someClaim\", \"intClaim\": 1, \"nestedClaim\": { \"listClaim\": [ \"1\", \"2\" ]}}";

        String jwt = JWTCreator.init()
                .withHeader(claimsJson)
                .sign(Algorithm.HMAC256("secret"));

        assertThat(jwt, is(notNullValue()));
        String[] parts = jwt.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);

        assertThat(headerJson, JsonMatcher.hasEntry("stringClaim", stringClaim));
        assertThat(headerJson, JsonMatcher.hasEntry("intClaim", intClaim));
        assertThat(headerJson, JsonMatcher.hasEntry("listClaim", nestedListClaims));
    }

    @Test
    public void shouldFailWithIllegalArgumentExceptionForInvalidJsonForHeaderClaims() {
        String invalidJson = "{ invalidJson }";

        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid header JSON");

        JWTCreator.init()
                .withHeader(invalidJson)
                .sign(Algorithm.HMAC256("secret"));
    }

    @Test
    public void shouldOverwriteExistingHeaderIfHeaderMapContainsTheSameKey() {
        Map<String, Object> header = new HashMap<>();
        header.put(HeaderParams.KEY_ID, "xyz");

        String signed = JWTCreator.init()
                .withKeyId("abc")
                .withHeader(header)
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "xyz"));
    }


    @Test
    public void shouldOverwriteExistingHeadersWhenSettingSameHeaderKey() {
        Map<String, Object> header = new HashMap<>();
        header.put(HeaderParams.KEY_ID, "xyz");

        String signed = JWTCreator.init()
                .withHeader(header)
                .withKeyId("abc")
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "abc"));
    }

    @Test
    public void shouldRemoveHeaderIfTheValueIsNull() {
        Map<String, Object> header = new HashMap<>();
        header.put(HeaderParams.KEY_ID, null);
        header.put("test2", "isSet");

        String signed = JWTCreator.init()
                .withKeyId("test")
                .withHeader(header)
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.isNotPresent(HeaderParams.KEY_ID));
        assertThat(headerJson, JsonMatcher.hasEntry("test2", "isSet"));
    }

    @Test
    public void shouldAddKeyId() {
        String signed = JWTCreator.init()
                .withKeyId("56a8bd44da435300010000015f5ed")
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry("kid", "56a8bd44da435300010000015f5ed"));
    }

    @Test
    public void shouldAddKeyIdIfAvailableFromRSAAlgorithms() throws Exception {
        RSAPrivateKey privateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_RSA, "RSA");
        RSAKeyProvider provider = mock(RSAKeyProvider.class);
        when(provider.getPrivateKeyId()).thenReturn("my-key-id");
        when(provider.getPrivateKey()).thenReturn(privateKey);

        String signed = JWTCreator.init()
                .sign(Algorithm.RSA256(provider));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id"));
    }

    @Test
    public void shouldNotOverwriteKeyIdIfAddedFromRSAAlgorithms() throws Exception {
        RSAPrivateKey privateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_RSA, "RSA");
        RSAKeyProvider provider = mock(RSAKeyProvider.class);
        when(provider.getPrivateKeyId()).thenReturn("my-key-id");
        when(provider.getPrivateKey()).thenReturn(privateKey);

        String signed = JWTCreator.init()
                .withKeyId("real-key-id")
                .sign(Algorithm.RSA256(provider));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id"));
    }

    @Test
    public void shouldAddKeyIdIfAvailableFromECDSAAlgorithms() throws Exception {
        ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256, "EC");
        ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class);
        when(provider.getPrivateKeyId()).thenReturn("my-key-id");
        when(provider.getPrivateKey()).thenReturn(privateKey);

        String signed = JWTCreator.init()
                .sign(Algorithm.ECDSA256(provider));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id"));
    }

    @Test
    public void shouldNotOverwriteKeyIdIfAddedFromECDSAAlgorithms() throws Exception {
        ECPrivateKey privateKey = (ECPrivateKey) PemUtils.readPrivateKeyFromFile(PRIVATE_KEY_FILE_EC_256, "EC");
        ECDSAKeyProvider provider = mock(ECDSAKeyProvider.class);
        when(provider.getPrivateKeyId()).thenReturn("my-key-id");
        when(provider.getPrivateKey()).thenReturn(privateKey);

        String signed = JWTCreator.init()
                .withKeyId("real-key-id")
                .sign(Algorithm.ECDSA256(provider));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry("kid", "my-key-id"));
    }

    @Test
    public void shouldAddIssuer() {
        String signed = JWTCreator.init()
                .withIssuer("auth0")
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJpc3MiOiJhdXRoMCJ9"));
    }

    @Test
    public void shouldAddSubject() {
        String signed = JWTCreator.init()
                .withSubject("1234567890")
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJzdWIiOiIxMjM0NTY3ODkwIn0"));
    }

    @Test
    public void shouldAddAudience() {
        String signed = JWTCreator.init()
                .withAudience("Mark")
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJhdWQiOiJNYXJrIn0"));


        String signedArr = JWTCreator.init()
                .withAudience("Mark", "David")
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signedArr, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signedArr)[1], is("eyJhdWQiOlsiTWFyayIsIkRhdmlkIl19"));
    }

    @Test
    public void shouldAddExpiresAt() {
        String signed = JWTCreator.init()
                .withExpiresAt(new Date(1477592000))
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJleHAiOjE0Nzc1OTJ9"));
    }

    @Test
    public void shouldAddExpiresAtInstant() {
        String signed = JWTCreator.init()
                .withExpiresAt(Instant.ofEpochSecond(1477592))
                .sign(Algorithm.HMAC256("secret"));

        System.out.println(signed);
        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJleHAiOjE0Nzc1OTJ9"));
    }

    @Test
    public void shouldAddNotBefore() {
        String signed = JWTCreator.init()
                .withNotBefore(new Date(1477592000))
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJuYmYiOjE0Nzc1OTJ9"));
    }

    @Test
    public void shouldAddNotBeforeInstant() {
        String signed = JWTCreator.init()
                .withNotBefore(Instant.ofEpochSecond(1477592))
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJuYmYiOjE0Nzc1OTJ9"));
    }

    @Test
    public void shouldAddIssuedAt() {
        String signed = JWTCreator.init()
                .withIssuedAt(new Date(1477592000))
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJpYXQiOjE0Nzc1OTJ9"));
    }

    @Test
    public void shouldAddIssuedAtInstant() {
        String signed = JWTCreator.init()
                .withIssuedAt(Instant.ofEpochSecond(1477592))
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJpYXQiOjE0Nzc1OTJ9"));
    }

    @Test
    public void shouldAddJWTId() {
        String signed = JWTCreator.init()
                .withJWTId("jwt_id_123")
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[1], is("eyJqdGkiOiJqd3RfaWRfMTIzIn0"));
    }

    @Test
    public void shouldSetCorrectAlgorithmInTheHeader() {
        String signed = JWTCreator.init()
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry("alg", "HS256"));
    }

    @Test
    public void shouldSetDefaultTypeInTheHeader() {
        String signed = JWTCreator.init()
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry("typ", "JWT"));
    }

    @Test
    public void shouldSetCustomTypeInTheHeader() {
        Map<String, Object> header = Collections.singletonMap("typ", "passport");
        String signed = JWTCreator.init()
                .withHeader(header)
                .sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        assertThat(headerJson, JsonMatcher.hasEntry("typ", "passport"));
    }

    @Test
    public void shouldSetEmptySignatureIfAlgorithmIsNone() {
        String signed = JWTCreator.init()
                .sign(Algorithm.none());
        assertThat(signed, is(notNullValue()));
        assertThat(TokenUtils.splitToken(signed)[2], is(""));
    }

    @Test
    public void shouldThrowOnNullCustomClaimName() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("The Custom Claim's name can't be null.");
        JWTCreator.init()
                .withClaim(null, "value");
    }

    @Test
    public void shouldAcceptCustomClaimOfTypeString() {
        String jwt = JWTCreator.init()
                .withClaim("name", "value")
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomClaimOfTypeInteger() {
        String jwt = JWTCreator.init()
                .withClaim("name", 123)
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomClaimOfTypeLong() {
        String jwt = JWTCreator.init()
                .withClaim("name", Long.MAX_VALUE)
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomClaimOfTypeDouble() {
        String jwt = JWTCreator.init()
                .withClaim("name", 23.45)
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomClaimOfTypeBoolean() {
        String jwt = JWTCreator.init()
                .withClaim("name", true)
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomClaimOfTypeDate() {
        Date date = new Date(1478891521000L);
        String jwt = JWTCreator.init()
                .withClaim("name", date)
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomClaimOfTypeDateInstant() {
        Instant instant = Instant.ofEpochSecond(1478891521);
        String jwt = JWTCreator.init()
                .withClaim("name", instant)
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomArrayClaimOfTypeString() {
        String jwt = JWTCreator.init()
                .withArrayClaim("name", new String[]{"text", "123", "true"})
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomArrayClaimOfTypeInteger() {
        String jwt = JWTCreator.init()
                .withArrayClaim("name", new Integer[]{1, 2, 3})
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomArrayClaimOfTypeLong() {
        String jwt = JWTCreator.init()
                .withArrayClaim("name", new Long[]{1L, 2L, 3L})
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void shouldAcceptCustomClaimOfTypeMap() {
        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() {
        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(123000L));
        data.put("instant", Instant.ofEpochSecond(123));
        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.getUrlDecoder().decode(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("instant"), is(123));
        assertThat(map.get("boolean"), is(true));

        // array types
        assertThat(map.get("intArray"), is(Arrays.asList(3, 5)));
        assertThat(map.get("longArray"), is(Arrays.asList(Long.MAX_VALUE, Long.MIN_VALUE)));
        assertThat(map.get("stringArray"), is(Collections.singletonList("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(123000L));
        data.add(Instant.ofEpochSecond(123));
        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.getUrlDecoder().decode(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(123));
        assertThat(list.get(6), is(true));

        // array types
        assertThat(list.get(7), is(Arrays.asList(3, 5)));
        assertThat(list.get(8), is(Arrays.asList(Long.MAX_VALUE, Long.MIN_VALUE)));
        assertThat(list.get(9), is(Arrays.asList("string")));

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

    @Test
    public void shouldAcceptCustomClaimForNullListItem() {
        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 shouldRefuseCustomClaimForNullMapKey() {
        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() {
        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() {
        List<Object> list = Collections.singletonList(new UserPojo("Michael", 255));

        exception.expect(IllegalArgumentException.class);

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

    @Test
    public void shouldRefuseCustomListClaimForUnknownListElementWrappedInAMap() {
        List<Object> list = Collections.singletonList(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() {
        List<Object> list = new ArrayList<>();
        list.add(new Object[]{"test"});

        exception.expect(IllegalArgumentException.class);

        JWTCreator.init()
                .withClaim("list", list)
                .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.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
        assertThat(payloadJson, JsonMatcher.hasEntry("asd", 123));
    }

    @Test
    public void withPayloadShouldCreateJwtWithEmptyBodyIfPayloadNull() {
        Map<String, Object> nullMap = null;
        String nullString = null;
        String jwt = JWTCreator.init()
                .withPayload(nullMap)
                .withPayload(nullString)
                .sign(Algorithm.HMAC256("secret"));

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

    @Test
    public void withPayloadShouldOverwriteExistingClaimIfPayloadMapContainsTheSameKey() {
        Map<String, Object> payload = new HashMap<>();
        payload.put(HeaderParams.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.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
        assertThat(payloadJson, JsonMatcher.hasEntry(HeaderParams.KEY_ID, "xyz"));
    }

    @Test
    public void shouldOverwriteExistingPayloadWhenSettingSamePayloadKey() {
        Map<String, Object> payload = new HashMap<>();
        payload.put(RegisteredClaims.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.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
        assertThat(payloadJson, JsonMatcher.hasEntry(RegisteredClaims.ISSUER, "abc"));
    }

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

        Map<String, Object> payload = new HashMap<>();
        payload.put("entry", "value");
        payload.put("pojo", new UserPojo("name", 42));
        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.getUrlDecoder().decode(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, Date, Instant, and Null");

        Map<String, Object> payload = new HashMap<>();
        payload.put("list", Arrays.asList("item1", new UserPojo("name", 42)));
        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, Date, Instant, and Null");

        Map<String, Object> payload = new HashMap<>();
        payload.put("entry", "value");
        payload.put("map", Collections.singletonMap("pojo", new UserPojo("name", 42)));
        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.getUrlDecoder().decode(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));
    }

    @Test
    public void withPayloadShouldSupportNullValuesEverywhere() {
         /*
        JWT:
            {
              "listClaim": [
                "answer to ultimate question of life",
                42,
                null
              ],
              "claim": null,
              "listNestedClaim": [
                1,
                2,
                {
                  "nestedObjKey": null
                }
              ],
              "objClaim": {
                "nestedObjKey": null,
                "objObjKey": {
                  "nestedObjKey": null,
                  "objListKey": [
                    null,
                    "nestedList2"
                  ]
                },
                "objListKey": [
                  null,
                  "nestedList2"
                ]
              }
            }
         */

        List<?> listClaim = Arrays.asList("answer to ultimate question of life", 42, null);
        List<?> listNestedClaim = Arrays.asList(1, 2, Collections.singletonMap("nestedObjKey", null));
        List<?> objListKey = Arrays.asList(null, "nestedList2");
        HashMap<String, Object> objClaim = new HashMap<>();
        objClaim.put("nestedObjKey", null);
        objClaim.put("objListKey", objListKey);
        objClaim.put("objObjKey", new HashMap<>(objClaim));


        Map<String, Object> payload = new HashMap<>();
        payload.put("claim", null);
        payload.put("listClaim", listClaim);
        payload.put("listNestedClaim", listNestedClaim);
        payload.put("objClaim", objClaim);

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

        assertThat(jwt, is(notNullValue()));
        String[] parts = jwt.split("\\.");
        String payloadJson = new String(Base64.getUrlDecoder().decode(parts[1]), StandardCharsets.UTF_8);
        String headerJson = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);

        assertThat(payloadJson, JsonMatcher.hasEntry("claim", null));
        assertThat(payloadJson, JsonMatcher.hasEntry("listClaim", listClaim));
        assertThat(payloadJson, JsonMatcher.hasEntry("listNestedClaim", listNestedClaim));
        assertThat(payloadJson, JsonMatcher.hasEntry("objClaim", objClaim));

        assertThat(headerJson, JsonMatcher.hasEntry("claim", null));
        assertThat(headerJson, JsonMatcher.hasEntry("listClaim", listClaim));
        assertThat(headerJson, JsonMatcher.hasEntry("listNestedClaim", listNestedClaim));
        assertThat(headerJson, JsonMatcher.hasEntry("objClaim", objClaim));
    }

    @Test
    public void withPayloadShouldSupportJsonValueWithNestedDataStructure() {
        String stringClaim = "someClaim";
        Integer intClaim = 1;
        List<String> nestedListClaims = Arrays.asList("1", "2");
        String claimsJson = "{\"stringClaim\": \"someClaim\", \"intClaim\": 1, \"nestedClaim\": { \"listClaim\": [ \"1\", \"2\" ]}}";

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

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

        assertThat(payloadJson, JsonMatcher.hasEntry("stringClaim", stringClaim));
        assertThat(payloadJson, JsonMatcher.hasEntry("intClaim", intClaim));
        assertThat(payloadJson, JsonMatcher.hasEntry("listClaim", nestedListClaims));
    }

    @Test
    public void shouldFailWithIllegalArgumentExceptionForInvalidJsonForPayloadClaims() {
        String invalidJson = "{ invalidJson }";

        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Invalid payload JSON");

        JWTCreator.init()
                .withPayload(invalidJson)
                .sign(Algorithm.HMAC256("secret"));
    }

    @Test
    public void shouldCreatePayloadWithNullForMap() {
        String jwt = JWTCreator.init()
                .withClaim("name", (Map<String, ?>) null)
                .sign(Algorithm.HMAC256("secret"));
        assertThat(jwt, is(notNullValue()));
        assertTrue(JWT.decode(jwt).getClaim("name").isNull());
    }

    @Test
    public void shouldCreatePayloadWithNullForList() {
        String jwt = JWTCreator.init()
                .withClaim("name", (List<?>) null)
                .sign(Algorithm.HMAC256("secret"));
        assertThat(jwt, is(notNullValue()));
        assertTrue(JWT.decode(jwt).getClaim("name").isNull());
    }

    @Test
    public void shouldPreserveInsertionOrder() throws Exception {
        String taxonomyJson = "{\"class\": \"mammalia\", \"order\": \"carnivora\", \"family\": \"canidae\", \"genus\": \"vulpes\"}";
        List<String> taxonomyClaims = Arrays.asList("class", "order", "family", "genus");
        List<String> headerInsertionOrder = new ArrayList<>(taxonomyClaims);
        Map<String, Object> header = new LinkedHashMap<>();
        for (int i = 0; i < 10; i++) {
            String key = "h" + i;
            header.put(key, "v" + 1);
            headerInsertionOrder.add(key);
        }

        List<String> payloadInsertionOrder = new ArrayList<>(taxonomyClaims);
        JWTCreator.Builder builder = JWTCreator.init()
                .withHeader(taxonomyJson)
                .withHeader(header)
                .withPayload(taxonomyJson);
        for (int i = 0; i < 10; i++) {
            String name = "c" + i;
            builder = builder.withClaim(name, "v" + i);
            payloadInsertionOrder.add(name);
        }
        String signed = builder.sign(Algorithm.HMAC256("secret"));

        assertThat(signed, is(notNullValue()));
        String[] parts = signed.split("\\.");
        Base64.Decoder urlDecoder = Base64.getUrlDecoder();
        String headerJson = new String(urlDecoder.decode(parts[0]), StandardCharsets.UTF_8);
        String payloadJson = new String(urlDecoder.decode(parts[1]), StandardCharsets.UTF_8);

        ObjectMapper objectMapper = new ObjectMapper();

        List<String> headerFields = new ArrayList<>();
        objectMapper.readValue(headerJson, ObjectNode.class)
                .fieldNames().forEachRemaining(headerFields::add);
        headerFields.retainAll(headerInsertionOrder);
        assertThat("Header insertion order should be preserved",
                headerFields, is(equalTo(headerInsertionOrder)));

        List<String> payloadFields = new ArrayList<>();
        objectMapper.readValue(payloadJson, ObjectNode.class)
                .fieldNames().forEachRemaining(payloadFields::add);
        payloadFields.retainAll(payloadInsertionOrder);
        assertThat("Claim insertion order should be preserved",
                payloadFields, is(equalTo(payloadInsertionOrder)));
    }
}