HttpsJwksVerificationKeyResolverTest.java

/*
 * Copyright 2012-2017 Brian Campbell
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jose4j.keys.resolvers;

import org.jose4j.http.Get;
import org.jose4j.http.Response;
import org.jose4j.http.SimpleResponse;
import org.jose4j.jwk.*;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwx.JsonWebStructure;
import org.jose4j.lang.JoseException;
import org.jose4j.lang.UnresolvableKeyException;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.security.Key;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

/**
 *
 */
public class HttpsJwksVerificationKeyResolverTest
{
	private static final Logger log = LoggerFactory.getLogger(HttpsJwksVerificationKeyResolverTest.class);
	
    @Test
    public void simpleKeyFoundThenNotFoundAndRefreshToFindAndThenCantFind() throws Exception
    {
        String firstJkwsJson = "{\"keys\":[{\"kty\":\"EC\",\"kid\":\"k1\",\"x\":\"1u9oeAkLQJcAnrv_m4fupf-lF43yFqmNjMsrukKDhEE\",\"y\":\"RG0cyWzinUl8NpfVVw2DqfH6zRqU_yF6aL1swssNv4E\",\"crv\":\"P-256\"}]}";
        String secondJwkJson = "{\"keys\":[{\"kty\":\"EC\",\"kid\":\"k2\",\"x\":\"865vGRGnwRFf1YWFI-ODhHkQwYs7dc9VlI8zleEUqyA\",\"y\":\"W-7d1hvHrhNqNGVVNZjTUopIdaegL3jEjWOPX284AOk\",\"crv\":\"P-256\"}]}";

        JsonWebKeySet jwks = new JsonWebKeySet(firstJkwsJson);
        JsonWebKey k1 = jwks.getJsonWebKeys().iterator().next();

        jwks = new JsonWebKeySet(secondJwkJson);
        JsonWebKey k2 = jwks.getJsonWebKeys().iterator().next();

        String location = "https://www.example.org/";
        HttpsJwks httpsJkws = new HttpsJwks(location);

        Get mockGet = mock(Get.class);
        Map<String,List<String>> headers = Collections.emptyMap();
        SimpleResponse ok1 = new Response(200, "OK", headers, firstJkwsJson);
        SimpleResponse ok2 = new Response(200, "OK", headers, secondJwkJson);
        when(mockGet.get(location)).thenReturn(ok1, ok2);

        httpsJkws.setRefreshReprieveThreshold(0);
        httpsJkws.setSimpleHttpGet(mockGet);

        HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(httpsJkws);
        JsonWebSignature jws = new JsonWebSignature();
        jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
        jws.setKeyIdHeaderValue("k1");
        Key key = resolver.resolveKey(jws, Collections.<JsonWebStructure>emptyList());
        assertThat(key, equalTo(k1.getKey()));

        jws = new JsonWebSignature();
        jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
        jws.setKeyIdHeaderValue("k1");
        key = resolver.resolveKey(jws, Collections.<JsonWebStructure>emptyList());
        assertThat(key, equalTo(k1.getKey()));

        jws = new JsonWebSignature();
        jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
        jws.setKeyIdHeaderValue("k1");
        key = resolver.resolveKey(jws, Collections.<JsonWebStructure>emptyList());
        assertThat(key, equalTo(k1.getKey()));

        jws = new JsonWebSignature();
        jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
        jws.setKeyIdHeaderValue("k2");
        key = resolver.resolveKey(jws, Collections.<JsonWebStructure>emptyList());
        assertThat(key, equalTo(k2.getKey()));

        jws = new JsonWebSignature();
        jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
        jws.setKeyIdHeaderValue("k2");
        key = resolver.resolveKey(jws, Collections.<JsonWebStructure>emptyList());
        assertThat(key, equalTo(k2.getKey()));

        jws = new JsonWebSignature();
        jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
        jws.setKeyIdHeaderValue("nope");
        try
        {
            key = resolver.resolveKey(jws, Collections.<JsonWebStructure>emptyList());
            fail("shouldn't have resolved a key but got " + key);
        }
        catch (UnresolvableKeyException e)
        {
        	log.debug("this was expected and is okay: {}", e.toString() );
            assertFalse("do you really need UnresolvableKeyException inside a UnresolvableKeyException?", e.getCause() instanceof UnresolvableKeyException);
        }
    }

    @Test
    public void testAnEx() throws Exception
    {
        String location = "https://www.example.org/";

        Get mockGet = mock(Get.class);
        when(mockGet.get(location)).thenThrow(new IOException(location + "says 'no GET for you!'"));
        HttpsJwks httpsJkws = new HttpsJwks(location);
        httpsJkws.setSimpleHttpGet(mockGet);
        HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(httpsJkws);

        JsonWebSignature jws = new JsonWebSignature();
        jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
        jws.setKeyIdHeaderValue("nope");
        try
        {
            Key key = resolver.resolveKey(jws, Collections.<JsonWebStructure>emptyList());
            fail("shouldn't have resolved a key but got " + key);

        }
        catch (UnresolvableKeyException e)
        {
            log.debug("this was expected and is okay: {}", e.toString());
        }
    }

    @Test
    public void selectWithVerifySignatureDisambiguate() throws Exception
    {
        JsonWebSignature jwsWith1stEC = new JsonWebSignature();
        jwsWith1stEC.setCompactSerialization("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJtZSIsImV4cCI6MTQ5NDQzNzgwOSwiYXVkIjoidGhlIGF1ZGllbmNlIiwiaXNzIjoidGhlIGlzc3VlciJ9." +
                "04tBvYG5QeY8lniGnkZNHMW8b0OPCN6XHuK9g8fsOz8uA_r0Yk-biMkWG7ltOMCFSiiPvEu7jNWfWbk0v-hWOg");

        JsonWebSignature jwsWith2ndEC = new JsonWebSignature();
        jwsWith2ndEC.setCompactSerialization("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJtZSIsImV4cCI6MTQ5NDQzNzgwOSwiYXVkIjoidGhlIGF1ZGllbmNlIiwiaXNzIjoidGhlIGlzc3VlciJ9." +
                "uIRIFrhftV39qJNOdaL8LwrK1prIJIHsP7Gn6jJAVbE2Mx4IkwGzBXDLKMulM1IvKElmSyK_KBg8afywcxoApA");

        JsonWebSignature jwsWith3rdEC = new JsonWebSignature();
        jwsWith3rdEC.setCompactSerialization("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJtZSIsImV4cCI6MTQ5NDQzNzgwOSwiYXVkIjoidGhlIGF1ZGllbmNlIiwiaXNzIjoidGhlIGlzc3VlciJ9." +
                "21eYfC_ZNf1FQ1Dtvj4rUiM9jYPgf1zJfeE_b2fclgu36KAN141ICqVjNxQqlK_7Wbct_FDxgyHvej_LEigb2Q");

        JsonWebSignature jwsWith1stRsa = new JsonWebSignature();
        jwsWith1stRsa.setCompactSerialization("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtZSIsImV4cCI6MTQ5NDQzNzgwOSwiYXVkIjoidGhlIGF1ZGllbmNlIiwiaXNzIjoidGhlIGlzc3VlciJ9." +
                "aECOQefwSdjN1Sj7LWRBV3m1uuHOFDL02nFxMWifACMELrdYZ2i9W_c6Co0SQoJ5HUE0otA8b2mXQBxJ-azetXT4YiJYBpNbKk_H52KOUWvLoOYNwrTKylWjoTprAQpCr9KQWvjn3xrCoers4N63iCC1D9mKOCrUWFzDy-" +
                "-inXDj-5VlLWfCUhu8fjx_lotgUYQVD03Rm06P3OWGz5G_oksJ7VpxDDRAYt7zROgmjFDpSWmAtNEKoAlRTeKnZZSN0R71gznBsofs-jJ8zF0QcFOuAfqHVaDWnKwqS0aduZXm0s7rH61e4OwtQdTtFZqCPldUxlfC7uzvLhxgXrdLew");

        JsonWebSignature jwsWith2ndRSA = new JsonWebSignature();
        jwsWith2ndRSA.setCompactSerialization("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJtZSIsImV4cCI6MTQ5NDQzNzgwOSwiYXVkIjoidGhlIGF1ZGllbmNlIiwiaXNzIjoidGhlIGlzc3VlciJ9." +
                "pgBu9S8g7MC2BN9YNlWD9JhjzWbQVjqpmErW4hMFncKD8bUidIbMBJSI3URXvnMJrLrAC5eB2gb6DccF_txQaqX1X81JbTSdQ44_P1W-1uIIkfIXUvM6OXv48W-CPm8xGuetQ1ayHgU_1ljtdkbdUHZ6irgaeIrFMgZX0J" +
                "db9Eydnfhwvno2oGk3y6ruq2KgKABIdzgvJXfwdOFGn1z0CxwQSVDkFRLsMsBljTwfTd0v3G8OXT8WRMZMGVyAgtKVu3XJyrPNntVqrzdgQQma6S06Y9J9V9t0AlgEAn2B4TqMxYcu1Tjr7bBL_v83zEXhbdcFBYLfJg-LY5wE6rA-dA");


        JsonWebSignature jwsWithUnknownEC = new JsonWebSignature();
        jwsWithUnknownEC.setCompactSerialization("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJtZSIsImV4cCI6MTQ5NDQzOTEyNywiYXVkIjoidGhlIGF1ZGllbmNlIiwiaXNzIjoidGhlIGlzc3VlciJ9." +
                "UE4B0IVPRip-3TDKhNAadCuj_Bf5PlEAn9K94Zd7mP25WNZwxDbQpDElZTZSp-3ngPqQyPGj27emYRHhOnFSAQ");

        JsonWebSignature jwsWith384EC = new JsonWebSignature();
        jwsWith384EC.setCompactSerialization("eyJhbGciOiJFUzM4NCJ9.eyJzdWIiOiJtZSIsImV4cCI6MTQ5NDQzOTIzMSwiYXVkIjoidGhlIGF1ZGllbmNlIiwiaXNzIjoidGhlIGlzc3VlciJ9." +
                "NyRtG_eFmMLQ0XkW5kvdSpzYsm6P5M3U8EBFKIhD-jw8E7FOYw9PZ3_o1PWuLWH3XeArZMW7-bAIVxo2bHqJsSUtB6Tf0NWPtCpUF2c1vbuRXEXkGrCUmc4sKyOBjimC");

        String firstJkwsJson =
                "{\"keys\":[" +
                        "{\"kty\":\"EC\",\"x\":\"yd4yK8EJWNY-fyB0veOTNqDt_HqpPa45VTSJjIiI8vM\",\"y\":\"UspqZi9nPaUwBY8kD6MPDHslh5f6UMnAiXsg1l3i6UM\",\"crv\":\"P-256\"}," +
                        "{\"kty\":\"EC\",\"x\":\"3WPq7AnMkQekA1ogYFqNS5NBOXPs68xadKvtsn4pgas\",\"y\":\"CEvQFmGwKv96TQYRrgS-nFl9xWfN8PuLnIwBVmtpfp0\",\"crv\":\"P-256\"}" +
                        "]}";

        String secondJwksJson =
                "{\"keys\":[" +
                        "{\"kty\":\"EC\",\"x\":\"yd4yK8EJWNY-fyB0veOTNqDt_HqpPa45VTSJjIiI8vM\",\"y\":\"UspqZi9nPaUwBY8kD6MPDHslh5f6UMnAiXsg1l3i6UM\",\"crv\":\"P-256\"}," +
                        "{\"kty\":\"EC\",\"x\":\"3WPq7AnMkQekA1ogYFqNS5NBOXPs68xadKvtsn4pgas\",\"y\":\"CEvQFmGwKv96TQYRrgS-nFl9xWfN8PuLnIwBVmtpfp0\",\"crv\":\"P-256\"}," +
                        "{\"kty\":\"EC\",\"x\":\"DUYwuVdWtzfd2nkfQ7YEE_3ORRv3o0PYX39qNGVNlyA\",\"y\":\"qxxvewtvj61pnGDS7hWZ026oZehJxtQO3-9oVa6YdT8\",\"crv\":\"P-256\"}," +
                        "{\"kty\":\"RSA\",\"n\":\"mGOTvaqxy6AlxHXJFqQc5WSfH3Mjso0nlleF4a1ebSMgnqpmK_s6BSP0v9CyKyn_sBNpsH6dlOsks4qwb88SdvoWpMo2ZCIt8YlefirEaT9J8OQycxMv" +
                        "k7U1t6vCyN8Z68FrwhzzsmnNI_GC723OfMhcEZiRGNRJadPCMPfY3q5PgRrCjUS4v2hQjaicDpZETgbGxWNuNiIPk2CGhG3LJIUX4rx5zrFPQuUKH2Z1zH4E39i3Ab0WBATY0" +
                        "warvlImI5_rT-uCvvepnaQ6Mc4ImpS3anLNjfPlaNVajl5aRuzzRO77XePN-XzFJUVbC_v1-s2IcJf8uB-PMKAtRqz_kw\",\"e\":\"AQAB\"}," +
                        "{\"kty\":\"RSA\",\"n\":\"4SoqXJikILVhuwpeOYjbi_KGFXfvMaiBtoDm7nKsVc8ayQ4RBGbQdqHIt6gxSSTHrRSbQ2s5lAHfeyBJ9myQitCwxHFzjIDGcp5_u0wNWJbWUsDnbS-p" +
                        "wAQsZXZ3m6u_aDEC4sCTjOuotzwJniehVAkm2B1OnoYVhooKt9CTjVj1hwMf8Cpr171Vt559LyzUhRml6Se_AJWG_oFLV2c5ALCi2USfq2G_zoXFt9Kc93LJ9XoPy-hbQXA13" +
                        "OXwi9YL_BDLk8nd7QfaUgm77-j6RbOYg0l0PTloggw7km7M1D8iDASfkuII-Dzqedcm3KQb0Quo20HkirlIk67E-jOk6Q\",\"e\":\"AQAB\"}]}";

        JsonWebKeySet firstJkws = new JsonWebKeySet(firstJkwsJson);
        JsonWebKeySet secondJwks = new JsonWebKeySet(secondJwksJson);

        String location = "https://www.example.org/";
        HttpsJwks httpsJkws = new HttpsJwks(location);

        Get mockGet = mock(Get.class);
        Map<String,List<String>> headers = Collections.emptyMap();
        SimpleResponse ok1 = new Response(200, "OK", headers, firstJkwsJson);
        SimpleResponse ok2 = new Response(200, "OK", headers, secondJwksJson);
        when(mockGet.get(location)).thenReturn(ok1, ok2);

        httpsJkws.setRefreshReprieveThreshold(0);
        httpsJkws.setSimpleHttpGet(mockGet);

        HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(httpsJkws);
        resolver.setDisambiguateWithVerifySignature(true);
        Key resolvedKey = resolver.resolveKey(jwsWith2ndEC, Collections.<JsonWebStructure>emptyList());
        assertThat(firstJkws.getJsonWebKeys().get(1).getKey(), equalTo(resolvedKey));

        resolvedKey = resolver.resolveKey(jwsWith1stEC, Collections.<JsonWebStructure>emptyList());
        assertThat(firstJkws.getJsonWebKeys().get(0).getKey(), equalTo(resolvedKey));

        resolvedKey = resolver.resolveKey(jwsWith3rdEC, Collections.<JsonWebStructure>emptyList()); // this one will get the second JWKS
        assertThat(secondJwks.getJsonWebKeys().get(2).getKey(), equalTo(resolvedKey));

        resolvedKey = resolver.resolveKey(jwsWith1stRsa, Collections.<JsonWebStructure>emptyList());
        assertThat(secondJwks.getJsonWebKeys().get(3).getKey(), equalTo(resolvedKey));

        resolvedKey = resolver.resolveKey(jwsWith2ndRSA, Collections.<JsonWebStructure>emptyList());
        assertThat(secondJwks.getJsonWebKeys().get(4).getKey(), equalTo(resolvedKey));

        try
        {
            resolvedKey = resolver.resolveKey(jwsWithUnknownEC, Collections.<JsonWebStructure>emptyList());
            fail("shouldn't have resolved a key but got " + resolvedKey);

        }
        catch (UnresolvableKeyException e)
        {
            log.debug("this was expected and is okay: {}", e.toString());
        }

        try
        {
            resolvedKey = resolver.resolveKey(jwsWith384EC, Collections.<JsonWebStructure>emptyList());
            fail("shouldn't have resolved a key but got " + resolvedKey);
        }
        catch (UnresolvableKeyException e)
        {
            log.debug("this was expected and is okay: {}", e.toString());
        }

    }

    @Test
    public void conformityByExtension() throws Exception
    {
        // OpenID Connect Conformance Profiles v3.0 apparently has a somewhat overzealous test to
        // Reject an ID Token without kid, if multiple JWKs are supplied in jwks_uri.
        // This test shows how to do that using a little subclass of HttpsJwksVerificationKeyResolver.
        // https://bitbucket.org/b_c/jose4j/issues/162 was asking for such a check to be put into VerificationJwkSelector
        // but that would impact and break legitimate cases (and numerous unit tests).
        // This shows a way to meet that Conformance test requirement when needed without breaking other usage.
        String jj = "{\"keys\":[" +
                        "{\"kty\":\"EC\",\"x\":\"yd4yK8EJWNY-fyB0veOTNqDt_HqpPa45VTSJjIiI8vM\",\"y\":\"UspqZi9nPaUwBY8kD6MPDHslh5f6UMnAiXsg1l3i6UM\",\"crv\":\"P-256\"}," +
                        "{\"kty\":\"EC\",\"x\":\"3WPq7AnMkQekA1ogYFqNS5NBOXPs68xadKvtsn4pgas\",\"y\":\"CEvQFmGwKv96TQYRrgS-nFl9xWfN8PuLnIwBVmtpfp0\",\"crv\":\"P-256\"}" +
                        "]}";

        JsonWebKeySet jwks = new JsonWebKeySet(jj);


        String location = "https://abc.bfe.xyz/";
        HttpsJwks httpsJkws = new HttpsJwks(location);

        Get mockGet = mock(Get.class);
        Map<String,List<String>> headers = Collections.emptyMap();
        SimpleResponse ok1 = new Response(200, "OK", headers, jj);
        when(mockGet.get(location)).thenReturn(ok1);

        httpsJkws.setRefreshReprieveThreshold(0);
        httpsJkws.setSimpleHttpGet(mockGet);


        JsonWebSignature jws = new JsonWebSignature();
        // no kid header
        jws.setCompactSerialization("eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJtZSIsImV4cCI6MTQ5NDQzNzgwOSwiYXVkIjoidGhlIGF1ZGllbmNlIiwiaXNzIjoidGhlIGlzc3VlciJ9." +
                "uIRIFrhftV39qJNOdaL8LwrK1prIJIHsP7Gn6jJAVbE2Mx4IkwGzBXDLKMulM1IvKElmSyK_KBg8afywcxoApA");

        HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(httpsJkws);
        resolver.setDisambiguateWithVerifySignature(true);
        jws.setKey(resolver.resolveKey(jws, Collections.<JsonWebStructure>emptyList()));
        assertTrue(jws.verifySignature());

        CustomHttpsJwksVerificationKeyResolver customResolver = new CustomHttpsJwksVerificationKeyResolver(httpsJkws);
        try
        {
            Key key = customResolver.resolveKey(jws, Collections.<JsonWebStructure>emptyList());
            assertNull(key);
        }
        catch (UnresolvableKeyException e)
        {
            log.debug("We expected an exception out of the CustomHttpsJwksVerificationKeyResolver: " + e);
        }
    }

    public static class CustomHttpsJwksVerificationKeyResolver extends HttpsJwksVerificationKeyResolver
    {
        CustomHttpsJwksVerificationKeyResolver(HttpsJwks httpsJkws)
        {
            super(httpsJkws);
        }

        @Override
        protected JsonWebKey select(JsonWebSignature jws, List<JsonWebKey> jsonWebKeys) throws JoseException
        {
            if (jws.getKeyIdHeaderValue() == null && jsonWebKeys.size() > 1)
            {
                throw new UnresolvableKeyException("There are multiple keys in the referenced JWK Set document, but the kid value was not provided in the JOSE Header.");
            }
            return super.select(jws, jsonWebKeys);
        }
    }
}