Support for resolving keys from a HTTPS x509 certificate service

Issue #73 resolved
Former user created an issue

The org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver class resolves verification keys for a JWT from a org.jose4j.jwk.HttpsJwks object, which gets a JWKS over HTTPS per Open ID Connect.

I am using a Ping Federate token server which is issuing OAuth2 access tokens in the JWT format, and the (asymmetric, public) verification keys are made available as x509 certificates in a different manner than JWKS. See the last two field descriptions on this documentation: https://documentation.pingidentity.com/display/PF70/Configuring+JSON-Token+Management . The URL format seems like something that could be specified as a template.

Was that type of key resolution intentionally left out of jose4j, or not implemented out of lack of interest/awareness?

I can decode test tokens, find a range of key ids used for signing, and then copy and paste the public certificate returned from the token server into something that the org.jose4j.keys.resolvers.X509VerificationKeyResolver can deal with, but this leaves much to be desired as the tokens only list the key id in the JWT header rather than the x509 cert thumbprint, and thus I need to set the "try all the keys" option on the x509 verifier at that point. Also, when the certs are renewed based on expiration, or added/deleted based on use, I would need to manually make changes to the token server clients which are doing local token verification.

Comments (3)

  1. Brian Campbell repo owner

    As it happens, I built that stuff in PingFederate some years ago. The X.509 endpoints that PingFederate optionally exposes in support of JWT access token validation were developed before the JWK standard and applications of it like OpenID Connect's HTTPS JWKS URI had really stabilized. That kind of key resolution wasn't included in jose4j because, while it's conceptually similar to an HTTPS JWKS endpoint, it's not standardized to the same extent. Generally, I try to keep the scope of core stuff in jose4j relatively small, focused and standards based.

    The next release (8.2) of PingFederate actually has a new option to expose the certs and keys from JWT access token managers as an HTTPS JWKS endpoint to better align with standards (and integrate with jose4j more easily for that matter). So that may be an option in the future.

    With all that said, however, the key resolver functionality in jose4j is extensible and customizable so you can plug in your own implantation of the VerificationKeyResolver interface to do exactly what you need. The following code has a VerificationKeyResolver implementation wrapped in a little test I used to check it that also shows kinda how to use it. Hope this helps!

    import org.jose4j.http.Get;
    import org.jose4j.http.SimpleGet;
    import org.jose4j.http.SimpleResponse;
    import org.jose4j.jws.JsonWebSignature;
    import org.jose4j.jwt.JwtClaims;
    import org.jose4j.jwt.NumericDate;
    import org.jose4j.jwt.consumer.JwtConsumer;
    import org.jose4j.jwt.consumer.JwtConsumerBuilder;
    import org.jose4j.jwx.JsonWebStructure;
    import org.jose4j.keys.X509Util;
    import org.jose4j.keys.resolvers.VerificationKeyResolver;
    import org.jose4j.lang.JoseException;
    import org.jose4j.lang.UnresolvableKeyException;
    import org.junit.Test;
    
    import java.io.IOException;
    import java.io.UnsupportedEncodingException;
    import java.net.URLEncoder;
    import java.security.Key;
    import java.security.cert.X509Certificate;
    import java.util.Collections;
    import java.util.LinkedHashMap;
    import java.util.List;
    import java.util.Map;
    
    /**
     *
     */
    public class Issue73SupportForResolvingKeysFromHTTPSx509CertificateService
    {
        @Test
        public void sigh() throws Exception
        {
            String[] jwtAts = {
                "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9uZSJ9.eyJzIjoiam9obiIsImV4cCI6MTQ3MjE3MzkzNywic2NvcGUiOlsiYSIsImIiXSwiY2xpZW50X2lkIjoiYSIsImF1ZCI6InJzLWp3dDczIn0.d_ZxQSslrtUe_tNcf3NhFt1Lmvl75rzsaa8SdTt212WM0hGDy-tZV4vQnLdfPKyvtWxti_-UaNl_IIXIABPZs5_gIsyUv2OrK-rqDqXprXBuYOiBekjzcw0yw2HzI5tAnzcPtsa5JQzxjgr3J0FjmsbM5kTiAAh8tS7PdOzt2Es4TKrAE5F3omwtqPADP4JU40Jyk50RsXvlIYfBtkDa6vpNQ3X82n-5g8k30ds3vw0Ng57C75EWaGG279wJT1W1_nhMOVhxL4x0ENe74T7xhMVFdXTzajtpea5PQAt1i1GkE8DMSurvNaHnZW5lETHu1-Ym3obd4GrURBEEYubJ5w",
                "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9uZSJ9.eyJzIjoiam9obiIsImV4cCI6MTQ3MjE3Mzk3Nywic2NvcGUiOlsiYSIsImIiXSwiY2xpZW50X2lkIjoiYSIsImF1ZCI6InJzLWp3dDczIn0.LvJbvnPCZkz-RBdZ8s13uxBG8T8qC25aMaTTgVNNe8YMvU_l81ypBcx9Km149jv5SJzR1wO5kyPiegYM4LmUT4nfhHd65mKfJnJLMDfVQKbmIT6gPTHuo3RA6Y5gPbSnneuyJ6gPnWATBjKfcGqr6uEnIrD4vx-r7sxYlyxNhqYY7tDEsbG1YnSwWdfMPBSeb6y_tUFjYo6BVy2S3BVPIkQ89fqLAcW0dzo04X6_uJsS6KtcmwhMhVeWQ7yzDU_dBpWK55W3Sjtn_jBvQl0OSqRyAM8wli2vSuEpSeBiwbRnRzppsRiTonPfgbxah1v4UMVdY1z5GkBIG8oqlYVH3Q",
                "eyJhbGciOiJSUzI1NiIsImtpZCI6Im9uZSJ9.eyJzIjoiam9obiIsImV4cCI6MTQ3MjE3NDA5OCwic2NvcGUiOlsiYSIsImIiXSwiY2xpZW50X2lkIjoiYSIsImF1ZCI6InJzLWp3dDczIn0.aMjpSB1xeS6IsGNha3qeatE_TLES93i_HE7zUa3XmAuKLJl8sxdZPcVTuWXjKVmPBaErZfBGWcgjrmJE90hKqMkdLd-vIEIbjVQyU0X_eKvznOQvGZX6bKsvtXyEqND24mBRQzsX34tIWAa2l_lGQQmkDTTZ2SYnFw0ME41b5mymyfR55nVuwedzP5C2sAsHVUJDxrMnyh8Rl8uf5LAbPl7Er9fSw2LL4MaHy2sakm1l94PPsFl6uBD4zUQyVfyGzQV3R9F8TI1a0ptN8_P7yLp_QMIFdKn5rRMW6KNQpu-JYsswGyPCG2T0wOxSvuxLTa0FAFrtmrca6VDch4qkvw",
                "eyJhbGciOiJSUzI1NiIsImtpZCI6IjMifQ.eyJzIjoiam9obiIsImV4cCI6MTQ3MjE3NDE3MCwic2NvcGUiOlsiYSIsImIiXSwiY2xpZW50X2lkIjoiYSIsImF1ZCI6InJzLWp3dDczIn0.le_GqUlZGvxWZOXYmFR4t8Tys9m6SeCLXTxr2W4qPEFNphKa8fvedHIp28aFjkQKa_E8R08mCGm-3IMfTkx9EO1h_WFwALnQdIo-I-BJxXyx_22bTd2UeAUjsh0O04YHgMAtNX-1NzPmbjiNHbE44PHqJ-dzSmME2B8hXih7MxM4z12dr9gcpZf6ouo7tGHdokMhIssBUbUIU4M6ha6QEceBqQ56HRo8OC0_NQGOrA4fAuzUjzMZALrKg7LLoOadL-nSikpVyk8jqUiDQ_3OukuQI0BaIp85UdNfysShSMkILwr-JoE_pkcDe_fF1AvZgr6EetfJkQHPuowYDQSKZw",
                "eyJhbGciOiJSUzI1NiIsImtpZCI6IjMifQ.eyJzIjoiam9obiIsImV4cCI6MTQ3MjE3NDIwNiwic2NvcGUiOlsiYSIsImIiXSwiY2xpZW50X2lkIjoiYSIsImF1ZCI6InJzLWp3dDczIn0.JDpEH5TTTmYC9Mv0VJ7FSoxHK1MhfwJQTq-orsJQVH80uNosS6XV_S-f_MFJrF3ba4D8twpOOZk3D_JPo6yaIXLe5Bqo51NBaPgqIBZKt_88KFUQi2NlQwULyvku2x35qPAuD2SW7Yr-EnVNfxbTjJLynKPhUA4lf9hQF_wZdsTCtCUtmxlo5CllX7tcFgOp72imvT-f5rvGZYuGERoPMCN4YlF2fzhVO_1mHFY9uJpuUcF1UVPWJmM42vlKSn04fYgi9y7IJPXOxtyhw9mRDSlN99SmlXNuQ0cjqN2lom7bWPelNi_PA1r3vY1lSisTVzEAa_An46roi-0sRLkv9g",
                "eyJhbGciOiJSUzI1NiIsImtpZCI6IjMifQ.eyJzIjoiam9obiIsImV4cCI6MTQ3MjE3NDIzMCwic2NvcGUiOlsiYSIsImIiXSwiY2xpZW50X2lkIjoiYSIsImF1ZCI6InJzLWp3dDczIn0.BChUeEUXsiTp41fKImmOWvjcoFJj2ynE0WU2rQ0-zK4lKs2xHHXsDDJ1L-N0TscaRnhackcEr43laku77l84TXOkHUksZnkazQZuN7CgwW6ictcbOkGXgttz9gpGoui11vAqlQP0NPzrpczoHJ9786EVtlfgXRgbtKnOdT2YOx5UVtuwIkPkS8uI1vCdOtRjNrfQRPxTQ2Exz96x48-BZ5wXc5W8earea7uiehTMXvMQMOmNwKzKqzcuLf0wQYUiTE9NUST6JdrVKQF2cI4x93qfW-6VGSum8g19TnTHvD9KWYBhF1GL4cXW8qC216yoXJgeu8H35MEEw32AmLSmTA",
                "eyJhbGciOiJSUzI1NiIsImtpZCI6InR3byJ9.eyJzIjoiam9obiIsImV4cCI6MTQ3MjE3NDMzNSwic2NvcGUiOlsiYSIsImIiXSwiY2xpZW50X2lkIjoiYSIsImF1ZCI6InJzLWp3dDczIn0.tXbhRkt6fQOI2w4GhtrwiZnkRifhT-QiPbPYsFM0jrQ6zpKyylI67tHesYM2Iz3_EwvTrQdJK_t8gsCqKKB5CLJW_tUg41rc8yvdqxeYYH776H8M_rIu9mCuHPdcCejySt9ANCk2kuoA_hfNcjHBMWfxpsp6aHZVk-oRbVPaP6RXKUi0vC5ewtQwzwmYlUb6rhyUAnf1CbSj0Udoums3RgLBBE2JkPrwAO5fLOxzpG3WhCwp7ChzQqwekwWlU7JrUulVSy_nPytuX21s1Za5ogREMOUwi0aazCfhtd2vUxUaViQHPOxzatxvmkkYDcoVZrXPoKm-UFtQfIc-MSY9YA",
                "eyJhbGciOiJSUzI1NiIsImtpZCI6InR3byJ9.eyJzIjoiam9obiIsImV4cCI6MTQ3MjE3NDM0Niwic2NvcGUiOlsiYSIsImIiXSwiY2xpZW50X2lkIjoiYSIsImF1ZCI6InJzLWp3dDczIn0.SyurLWme4guMmdnKyaPLeLled-Wwkkx6npevENAT3AWkIjdfTRDc0K24OEaFycDe65WykCaErgSu8fcqslBqYWKcJVcrAigvt7Pw58bGqlpyE6_tztVOSO573qxKjXKLRNrGEbsE7p2dn_WzEYu-wpBX1a6-H1Lf-bR_Odz_-lvnPAF_nl9A2BxJv2Fug5AbXTWqxJ8_ZREeRjAFD0WGA5Da7ouPP2K080iMbCcZRlFy7xW_mxWEV7RbyLdDwENiUR5ijHqhoJGovQKrlpZwd0KaTPcx16QFKyoAf-mcS8hb1I5hjOocQdqlrYHHM83VWy9i2X4-znSr4GcEh8WvLQ",
                "eyJhbGciOiJSUzI1NiIsImtpZCI6InR3byJ9.eyJzIjoiam9obiIsImV4cCI6MTQ3MjE3NDM1Miwic2NvcGUiOlsiYSIsImIiXSwiY2xpZW50X2lkIjoiYSIsImF1ZCI6InJzLWp3dDczIn0.riIPv276j6f7RcQOcRQ-7uarx7L4H_2LdJrn-xeEZaseJXsw4S-ei7RbsITkTBNjT_gba1p6dlce942USX24iG5kmlsHxWWAl88ql29wwurPxguB0T6oqUr-T23uFXqojXPDTSxZi_YIXuYeoVwyuYaLsfmofw9kbxiJtaZjhy4kJZsF2At4vU7Oz_BLSxbyhD1iHR_kU8NMSw-CCZ728J0e6iEIONk80wO3FfH3vzrmemIXPrn-lyGJiFh7rbmZh46NVHvylF0r4w27vDSP27sIyqdQn79vf1EcdDg4_P0Gh6o7Xr9JufzETewcKhD-DuNWyioFBLUkZKR2i4YEug"
            };
    
            Get get = new Get();
            X509Certificate pfServerCert = new X509Util().fromBase64Der(
                    "MIICUDCCAbkCBETczdcwDQYJKoZIhvcNAQEFBQAwbzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNP\n" +
                    "MQ8wDQYDVQQHEwZEZW52ZXIxFTATBgNVBAoTDFBpbmdJZGVudGl0eTEXMBUGA1UECxMOQnJpYW4g\n" +
                    "Q2FtcGJlbGwxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0wNjA4MTExODM1MDNaFw0zMzEyMjcxODM1\n" +
                    "MDNaMG8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDTzEPMA0GA1UEBxMGRGVudmVyMRUwEwYDVQQK\n" +
                    "EwxQaW5nSWRlbnRpdHkxFzAVBgNVBAsTDkJyaWFuIENhbXBiZWxsMRIwEAYDVQQDEwlsb2NhbGhv\n" +
                    "c3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAJLrpeiY/Ai2gGFxNY8Tm/QSO8qgPOGKDMAT\n" +
                    "08QMyHRlxW8fpezfBTAtKcEsztPzwYTLWmf6opfJT+5N6cJKacxWchn/dRrzV2BoNuz1uo7wlpRq\n" +
                    "wcaOoi6yHuopNuNO1ms1vmlv3POq5qzMe6c1LRGADyZhi0KejDX6+jVaDiUTAgMBAAEwDQYJKoZI\n" +
                    "hvcNAQEFBQADgYEAMojbPEYJiIWgQzZcQJCQeodtKSJl5+lA8MWBBFFyZmvZ6jUYglIQdLlc8Pu6\n" +
                    "JF2j/hZEeTI87z/DOT6UuqZA83gZcy6re4wMnZvY2kWX9CsVWDCaZhnyhjBNYfhcOf0ZychoKSha\n" +
                    "EpTQ5UAGwvYYcbqIWC04GAZYVsZxlPl9hoA=");
            get.setTrustedCertificates(pfServerCert);
    
            PingFederateX509UrlKeyResolver resolver = new PingFederateX509UrlKeyResolver(get, "localhost:9031");
    
            JwtConsumer jwtConsumer = new JwtConsumerBuilder()
                    .setRequireExpirationTime()
                    .setExpectedAudience("rs-jwt73")
                    .setEvaluationTime(NumericDate.fromSeconds(1472171977))  // just for testing!!!!
                    .setVerificationKeyResolver(resolver)
                    .build();
    
            long start = System.currentTimeMillis();
    
            for (int i = 0 ; i < 1000; i++)
            {
                for (String at : jwtAts)
                {
                    JwtClaims jwtClaims = jwtConsumer.processToClaims(at);
                    System.out.println(jwtClaims);
                }
            }
    
            System.out.println(System.currentTimeMillis() - start);
        }
    
        public class PingFederateX509UrlKeyResolver implements VerificationKeyResolver
        {
            private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
            private static final String END_CERTIFICATE = "-----END CERTIFICATE-----";
    
            private SimpleGet get;
            private String host;
    
            private Map<String,X509Certificate> cache = Collections.synchronizedMap(new CacheMap());
    
            public PingFederateX509UrlKeyResolver(SimpleGet g, String host)
            {
                this.get = g;
                this.host = host;
            }
    
            @Override
            public Key resolveKey(JsonWebSignature jws, List<JsonWebStructure> nestingContext) throws UnresolvableKeyException
            {
    
                String kid = jws.getKeyIdHeaderValue();
    
                X509Certificate x509Certificate = cache.get(kid);
    
                if (x509Certificate == null)
                {
                    String encodedKid = kid;
                    try
                    {
                        encodedKid = URLEncoder.encode(kid, "UTF-8");
                    }
                    catch (UnsupportedEncodingException e)
                    {
                        // not gonna happen
                    }
    
                    String location = "https://"+host+"/ext/oauth/x509/kid?v=" + encodedKid;
    
                    String encodedCert;
                    String responseBody;
    
                    try
                    {
                        SimpleResponse simpleResponse = get.get(location);
                        responseBody = simpleResponse.getBody();
                        encodedCert = responseBody.substring(responseBody.indexOf(BEGIN_CERTIFICATE) + BEGIN_CERTIFICATE.length());
                        encodedCert = encodedCert.substring(0, encodedCert.indexOf(END_CERTIFICATE));
                    }
                    catch (IOException e)
                    {
                        throw new UnresolvableKeyException("Unable to get certificate from " + location, e);
                    }
    
                    try
                    {
                        X509Util x509Util = new X509Util();
                        x509Certificate = x509Util.fromBase64Der(encodedCert);
                        cache.put(kid, x509Certificate);
                    }
                    catch (JoseException e)
                    {
                        throw new UnresolvableKeyException("Unable to create certificate object from response body content " + responseBody, e);
                    }
                }
    
                return x509Certificate.getPublicKey();
            }
    
    
            private class CacheMap extends LinkedHashMap<String,X509Certificate>
            {
                @Override
                protected boolean removeEldestEntry(Map.Entry<String, X509Certificate> eldest)
                {
                    return size() > 10;
                }
            }
    
        }
    }
    
  2. GregD

    Okay thanks for the code! We're looking forward to that feature in the next version of the token server, which would make things nice and streamlined overall.

  3. Log in to comment