GoogsTooSmallKeyJwtConsumerTest.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.jwt.consumer;

import org.jose4j.jwk.JsonWebKeySet;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
import org.jose4j.keys.resolvers.JwksVerificationKeyResolver;
import org.jose4j.lang.JoseException;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;

/**
 *
 */
public class GoogsTooSmallKeyJwtConsumerTest
{
    /**
     * ~ May 2015 Google's JWKS URI https://www.googleapis.com/oauth2/v3/certs for OIDC had 1024 bit RSA keys in it that were being used to sign ID tokens.
     * That goes against the min of 2048 in https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.3
     * "A key of size 2048 bits or larger MUST be used with [RS256, etc]"
     *
     * These are some tests to check that we do, by default, enforce the key size (it's been that way for a long time) but that there are easy workarounds
     * possible at the JwtConsumer[Builder] layer.
     *
     * The example content was from Google May 14th '15
     *
     * A bug report was submitted to them on May 19 2-4355000007039 but we'll see if anything comes of it.  Exposing the setRelaxXXXKeyValidations on JwtConsumer[Builder]
     * will probably be useful in other ways.
     *
     * On July 8, 2015 I was informed that they moved to using 2048 bit RSA keys (thanks William!) and was asked to test it. The new test
     * here checks that things work as expected.
     */

    static String ID_TOKEN = "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijc2ZmQzMmFlYzdlMGY4YzE5MGRkYThiOWRkODVlN2NmNWFkMzNjNDMifQ" +
            ".eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTE2MzA4NDA4MzE0NjYxNDc4MTMyIiwiYXpwIjoiODIyNzM3NTU1NDI5LWV2dmtkMDBvdHFyNWdsMTEwbmZhcGlzamZvZWEzNmpmLmFwcHMuZ29vZ2xldXNlcmNvb" +
            "nRlbnQuY29tIiwiZW1haWwiOiJqa3Rlc3QxQG1hcml0aW1lc291cmNlLmNhIiwiYXRfaGFzaCI6Im85bUZjZUx6QV9ZMnhmNEJqVmdOQmciLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXVkIjoiODIyNzM3NTU1NDI5LWV2dmtkMDB" +
            "vdHFyNWdsMTEwbmZhcGlzamZvZWEzNmpmLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiaGQiOiJtYXJpdGltZXNvdXJjZS5jYSIsIm9wZW5pZF9pZCI6Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vYWNjb3VudHMvbzgvaWQ_a" +
            "WQ9QUl0T2F3bGIxSEhFZFJJZW00d2Z1MXFNY1BUdWZvUDZzTi11ZVVrIiwiaWF0IjoxNDMxNjEyMjM4LCJleHAiOjE0MzE2MTU4Mzh9" +
            ".RRMVpR9WJrkddegS4uKNT7rTov-LvRQ9sCtGo_SXrqkNbLZgArSJcmmHHxoQDsVWUjl2ZNG-7ZjDRuMu-POJLR4GHpwmQ8gttAEeywkiW4in5pUOb21AdgH29HDwG2mY6iVavsASHRutK747gURRlpt3wUJOJk00T9W2N0fVsTE";

    static String JWKS_JSON = "{ \"keys\": [\n" +
            "  {\n" +
            "   \"kty\": \"RSA\",\n" +
            "   \"alg\": \"RS256\",\n" +
            "   \"use\": \"sig\",\n" +
            "   \"kid\": \"76fd32aec7e0f8c190dda8b9dd85e7cf5ad33c43\",\n" +
            "   \"n\": \"03TVzpSoWDe8iPqvAde1JmmITIHD6JU8Koy10fSUW0u1QO6fle93GxHOHeQmP7FBhLSy5gWK23za38kN0KMucYGOjcWOwnO_pTQrCXxFzD-HBy_IiRyRkhuaQXsKvpJbblMEmcfeR4cWlzKt9RKjjXBl5bmIiLrN167iftlR84E\",\n" +
            "   \"e\": \"AQAB\"\n" +
            "  },\n" +
            "  {\n" +
            "   \"kty\": \"RSA\",\n" +
            "   \"alg\": \"RS256\",\n" +
            "   \"use\": \"sig\",\n" +
            "   \"kid\": \"317b5931c783031d970c1a2552266215598a9814\",\n" +
            "   \"n\": \"sxAi31Tz53-HtjmVlGpyNEGO8MtL-uvwdKDG__a-gPYE8WGEQQgpBXjjFqmIsfs-yd8YHYw0uCJwAu-ILT1AbhVTZiEEnrLKNTc_gPqfveZxnySJCguVx1pWpZ0q9cBMdgvetrbUfRO2Sz1YFgfD7k9BacWwOM-eiFtgrWwOTo8\",\n" +
            "   \"e\": \"AQAB\"\n" +
            "  }\n" +
            " ]\n" +
            "}";

    static final String CLIENT_ID = "822737555429-evvkd00otqr5gl110nfapisjfoea36jf.apps.googleusercontent.com";
    static final String ISSUER = "accounts.google.com";
    static final NumericDate EVALUATION_TIME = NumericDate.fromSeconds(1431612438);
    static final String SUBJECT_VALUE = "116308408314661478132";

    @Test
    public void strictByDefault() throws JoseException
    {
        JsonWebKeySet jwks = new JsonWebKeySet(JWKS_JSON);
        JwksVerificationKeyResolver verificationKeyResolver = new JwksVerificationKeyResolver(jwks.getJsonWebKeys());

        JwtConsumer jwtConsumer = new JwtConsumerBuilder()
                .setRequireExpirationTime() // the JWT must have an expiration time
                .setEvaluationTime(EVALUATION_TIME)
                .setRequireSubject() // the JWT must have a subject claim
                .setExpectedIssuer(ISSUER)
                .setExpectedAudience(CLIENT_ID) // to whom the JWT is intended for
                .setVerificationKeyResolver(verificationKeyResolver) // pretend to use Google's jwks endpoint to find the key for signature checks
                .build(); // create the JwtConsumer instance

        SimpleJwtConsumerTestHelp.expectProcessingFailure(ID_TOKEN, jwtConsumer);
    }


    @Test
    public void firstWorkaroundUsingTwoPass() throws Exception
    {
        // Build a JwtConsumer that doesn't check signatures or do any validation.
        JwtConsumer firstPassJwtConsumer = new JwtConsumerBuilder()
                .setSkipAllValidators()
                .setDisableRequireSignature()
                .setSkipSignatureVerification()
                .build();

        //The first JwtConsumer is basically just used to parse the JWT into a JwtContext object.
        JwtContext jwtContext = firstPassJwtConsumer.process(ID_TOKEN);

        // turn off key key validation (chiefly the enforcement of RSA 2048 as min key size) on the innermost JOSE object (the JWS)
        jwtContext.getJoseObjects().iterator().next().setDoKeyValidation(false);

        JsonWebKeySet jwks = new JsonWebKeySet(JWKS_JSON);
        JwksVerificationKeyResolver verificationKeyResolver = new JwksVerificationKeyResolver(jwks.getJsonWebKeys());

        JwtConsumer jwtConsumer = new JwtConsumerBuilder()
                .setRequireExpirationTime() // the JWT must have an expiration time
                .setEvaluationTime(EVALUATION_TIME)
                .setRequireSubject() // the JWT must have a subject claim
                .setExpectedIssuer(ISSUER)
                .setExpectedAudience(CLIENT_ID) // to whom the JWT is intended for
                .setVerificationKeyResolver(verificationKeyResolver) // pretend to use Google's jwks endpoint to find the key for signature checks
                .build(); // create the JwtConsumer instance

        jwtConsumer.processContext(jwtContext);
        JwtClaims jwtClaims = jwtContext.getJwtClaims();
        assertThat(SUBJECT_VALUE, equalTo(jwtClaims.getSubject()));
    }

    @Test
    public void newerWorkaroundOnConsumerBuilder() throws Exception
    {
        JsonWebKeySet jwks = new JsonWebKeySet(JWKS_JSON);
        JwksVerificationKeyResolver verificationKeyResolver = new JwksVerificationKeyResolver(jwks.getJsonWebKeys());

        JwtConsumer jwtConsumer = new JwtConsumerBuilder()
                .setRelaxVerificationKeyValidation() // **THIS** is what will tell the underlying JWS to not check the key too much and allow the 1024
                .setRequireExpirationTime()
                .setEvaluationTime(EVALUATION_TIME)
                .setRequireSubject() // the JWT must have a subject claim
                .setExpectedIssuer(ISSUER)
                .setExpectedAudience(CLIENT_ID) // to whom the JWT is intended for
                .setVerificationKeyResolver(verificationKeyResolver) // pretend to use Google's jwks endpoint to find the key for signature checks
                .build(); // create the JwtConsumer instance

        JwtClaims claims = jwtConsumer.processToClaims(ID_TOKEN);
        assertThat(SUBJECT_VALUE, equalTo(claims.getSubject()));
    }

    @Test
    public void testAfterTheyMovedTo2048() throws Exception
    {
        // endpoints mentioned were found at https://accounts.google.com/.well-known/openid-configuration

        // JWKS content from https://www.googleapis.com/oauth2/v3/certs on July 8, 2015
        JsonWebKeySet jwks = new JsonWebKeySet("{\n" +
                " \"keys\": [\n" +
                "  {\n" +
                "   \"kty\": \"RSA\",\n" +
                "   \"alg\": \"RS256\",\n" +
                "   \"use\": \"sig\",\n" +
                "   \"kid\": \"e53139984bd36d2c230552441608cc0b5179487a\",\n" +
                "   \"n\": \"w5F_3au2fyRLapW4K1g0zT6hjF-co8hjHJWniH3aBOKP45xuSRYXnPrpBHkXM6jFkVHs2pCFAOg6o0tl65iRCcf3hOAI6VOIXjMCJqxNap0-j_lJ6Bc6TBKgX3XD96iEI92iaxn_UIVZ_SpPrbPVyRmH0P7B6oDkwFpApviJRtQzv1F6uyh9W_sNnEZrCZDcs5lL5Xa_44-EkhVNz8yGZmAz9d04htNU7xElmXKs8fRdospyv380WeaWFoNJpc-3ojgRus26jvPy8Oc-d4M5yqs9mI72-1G0zbGVFI_PfxZRL8YdFAIZLg44zGzL2M7pFmagJ7Aj46LUb3p_n9V1NQ\",\n" +
                "   \"e\": \"AQAB\"\n" +
                "  },\n" +
                "  {\n" +
                "   \"kty\": \"RSA\",\n" +
                "   \"alg\": \"RS256\",\n" +
                "   \"use\": \"sig\",\n" +
                "   \"kid\": \"bc8a31927af20860418f6b2231bbfd7ebcc04665\",\n" +
                "   \"n\": \"ucGr4fFCJYGVUwHYWAtBNclebyhMjALOTUmmAXdMrCIOgT8TxBEn5oXCrszWX7RoC37nFqc1GlMorfII19qMwHdC_iskju3Rh-AuHr29zkDpYIuh4lRW0xJ0Xyo2Iw4PlV9qgqPJLfkmE5V-sr5RxZNe0T1jyYaOGIJ5nF3WbDkgYW4GNHXhv-5tOwWLThJRtH_n6wtYqsBwqAdVX-EVbkyZvYeOzbiNiop7bDM5Td6ER1oCBC4NZjvjdmnOh8-_x6vB449jL5IRAOIIv8NW9dLtQd2DescZOw46HZjWO-zwyhjQeYY87R93yM9yivJdfrjQxydgEs8Ckh03NDATmQ\",\n" +
                "   \"e\": \"AQAB\"\n" +
                "  }\n" +
                " ]\n" +
                "}\n");

        // an ID token from making an openid request to https://accounts.google.com/o/oauth2/v2/auth
        String jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImJjOGEzMTkyN2FmMjA4NjA0MThmNmIyMjMxYmJmZDdlYmNjMDQ2NjUifQ" +
                ".eyJpc3MiOiJhY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTA5MzM5ODA3NjQ3Nzc3MzkzOTYxIiwiYXpwIjoiMTA3ODQ0OTAyOTY4Ni5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImF1ZCI6IjEwNzg0NDkwMjk2ODYuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJpYXQiOjE0MzYzODUzMzYsImV4cCI6MTQzNjM4ODkzNn0" +
                ".B8jAoYKnsN0Xy62VjBXIrk5B-3ZdbQNt_qzndhlOJXpo4W0C1Q4BvC8YjFc2k6T1qNuehfSrO9xvm-BQGAXRyuQSZPpcQOtP2_LR39oYpnBgDwGKxTdJwAHTIoYTti1R1o-sAkMk-dt4lP45RbUXJEKST0RLKe9RdjNKLtcg62wSvVuLwaqRYyIRWK3Tb8aRA3Eay8uUe8Llk5qJ-1E1pSOscvlYF6EVNkafKBa4jC5utAu5WwvdDoMFz3ZPOzNnhQsjOdxtnAjN3mI9EWNALUsLrdY54-O0JnVJGywKEnwfeDBcUClt_ZBwV-Rl8WMv8TWZRJ8SWywnYi2gaBnaPw";

        JwksVerificationKeyResolver verificationKeyResolver = new JwksVerificationKeyResolver(jwks.getJsonWebKeys());

        JwtConsumer jwtConsumer = new JwtConsumerBuilder()
                .setRequireExpirationTime()
                .setEvaluationTime(NumericDate.fromSeconds(1436388930))
                .setRequireSubject()
                .setExpectedIssuer(ISSUER)
                .setExpectedAudience("1078449029686.apps.googleusercontent.com") // borrowed a bitbucket client id
                .setVerificationKeyResolver(verificationKeyResolver) // pretend to use Google's jwks endpoint to find the key for signature checks
                .build();

        JwtClaims claims = jwtConsumer.processToClaims(jwt);
        assertThat("109339807647777393961", equalTo(claims.getSubject()));
    }

}