javajwtjwkjjwt

How to parse a RFC7517 JWK Set with JJWT?


I have an OpenID Connect Certificate endpoint returning the following JSON:

{
  "keys": [
    {
      "kid": "key1",
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "n": "jki4-Fw66lIy6oHk_YHLReGkdX3QkiizGUQHGeG_xjQUbwlOFejYm-CsMjWEpZcohX0BQVZomnrMCZC_qjNy-Tg5AIFcQZGXehT4kH_DXQZZR4OgT3uKvEEbMEYhZMPj5Bs9--420ONvCLMTU720UXqSF9IrXsuxtRZuaijwkMpQ2t9nIuJ6NKo_CBJHyeVvfLKN3a83Zi-6It6dkiLsOSvhQfbUAsr0NeKAobwmqGt9lT_K_JoLRVqTzFEC-XT7keobMdT9cKba2ML7Yz982Tr5BuGLXZTm7nfPKdk9Bi68HnO82Aas19_D5HJRieW7FqeqwE5MVl6E3IFt8HKblw",
      "e": "AQAB",
      "x5c": [
        "MIICqTCCAZECBgFxOn9tejANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1hY2FkZW1pY2Nsb3VkMB4XDTIwMDQwMjEwNDQyMVoXDTMwMDQwMjEwNDYwMVowGDEWMBQGA1UEAwwNYWNhZGVtaWNjbG91ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI5IuPhcOupSMuqB5P2By0XhpHV90JIosxlEBxnhv8Y0FG8JThXo2JvgrDI1hKWXKIV9AUFWaJp6zAmQv6ozcvk4OQCBXEGRl3oU+JB/w10GWUeDoE97irxBGzBGIWTD4+QbPfvuNtDjbwizE1O9tFF6khfSK17LsbUWbmoo8JDKUNrfZyLiejSqPwgSR8nlb3yyjd2vN2YvuiLenZIi7Dkr4UH21ALK9DXigKG8JqhrfZU/yvyaC0Vak8xRAvl0+5HqGzHU/XCm2tjC+2M/fNk6+Qbhi12U5u53zynZPQYuvB5zvNgGrNffw+RyUYnluxanqsBOTFZehNyBbfBym5cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJ+N/5p1bzHGgBT45cTe23Q3n0j8xogrJKk0LJLmcZTOU2nkcQhZeL4bpw/7eJasgnXCIMk37Ti8xiZKLPhSrRg0BcNIrKmrtA+2x6jDRIc0DoU3P83fM5h4ShUJAW+aKOx7JpV3E0KkOPzHbCRCB2w7oCleZHG9lJAGkcHAQQ01aIfyU3ow66kdAHyB6sAAnRXMf6aogTguqPVB8uAE3VTAWZDiPwyhqo/IVWDMs73bUty8qLDDj4Ei0Q+DcND3WghyeOGm8lCmAzPgl/zzphkQ6P+4Sq1gW06yfVvj862CuY9i6oBTldtAUvjKxCzl+QS8KzQBnB0oUzaFh8vHKYw=="
      ],
      "x5t": "6yqDA3RjYFZrc3DVcyVrvkNiMA0",
      "x5t#S256": "ifr81ikFJWOiNf2FDgW8dSOu5HEajKuwfGIw78G--Jw"
    },
    {
      "kid": "key2",
      "kty": "RSA",
      "alg": "RS512",
      "use": "sig",
      "n": "kKrdBB_DT37-GT75n_HdOSS0wxXIuheahBdJwTCHmB2Uk3IATjOpiFZjB8qAZ5d00AUu-oZrHG2VgnJq1jUiSb-RDYJTwA1lFXKEJu5CZwAB9xlfCFRXqPs9AL3-2l9-i5ajkMbSE10-S3dacwsrCFd-FL7w0428K7DHjtdwA0mWCyZW6nqWc7lutXhIfFlSmo7GY8M9tuMoOAOXOnLa0MYGh6G1jGvK8pNEyBTnKNEWqAIZhH8ENPMLm4vNFkuenFnP5VzcFNppwt3FnVFetnwudFPfEzUeHqyH7EsdOooFbD4IBu1iWXdI09uGCIJ30BJ2Q-OpXLGMur_YhXMb-Q",
      "e": "AQAB",
      "x5c": [
        "MIICqTCCAZECBgGEexA7YjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1hY2FkZW1pY2Nsb3VkMB4XDTIyMTExNTExMzExMloXDTMyMTExNTExMzI1MlowGDEWMBQGA1UEAwwNYWNhZGVtaWNjbG91ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJCq3QQfw09+/hk++Z/x3TkktMMVyLoXmoQXScEwh5gdlJNyAE4zqYhWYwfKgGeXdNAFLvqGaxxtlYJyatY1Ikm/kQ2CU8ANZRVyhCbuQmcAAfcZXwhUV6j7PQC9/tpffouWo5DG0hNdPkt3WnMLKwhXfhS+8NONvCuwx47XcANJlgsmVup6lnO5brV4SHxZUpqOxmPDPbbjKDgDlzpy2tDGBoehtYxryvKTRMgU5yjRFqgCGYR/BDTzC5uLzRZLnpxZz+Vc3BTaacLdxZ1RXrZ8LnRT3xM1Hh6sh+xLHTqKBWw+CAbtYll3SNPbhgiCd9ASdkPjqVyxjLq/2IVzG/kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQ4kzB7EwhUTGSr/IpAQa+viF7uYGxk+Iiec/s+ShfkFLoMPw+y9l5alwvnKAgTI1Pxrjha8Hu2OsCtGtu8ziJNd65VQiNoFsZp71WGq0+7+Zcqmk182CmjqoN+io7yfgg7N5/VygquHIY3aNB4riruQrbR33fQ49mjpZIM2eohU1teycfpwPCObTRGg5jZg+iUREy01k+QZplxgOqgyqrtTDUKoxZr8WwAWjlCBWyylOT5eEA/777yObYogmfrpNovo+dw1szaHB4BGfX1S522UUfRDAMtOTsjfCnusYEZsMUWXJe46ZLSYJsmIpGw4UwSC7I371elrC/dTzCSREpQ=="
      ],
      "x5t": "PwDhZrjmydYTXbB3skSdfamw5Xk",
      "x5t#S256": "44mNK5ARKWGn8S7R0tfEeJ6SVqpLp73VSeQeG2wMATE"
    }
  ]
}

I assume this is a RFC7517 JWK Set.

I am trying to parse this as a io.jsonwebtoken.security.JwkSet using the Gson deserializer implementation.

Maven dependencies:

    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-api</artifactId>
      <version>0.12.6</version>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-impl</artifactId>
      <version>0.12.6</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-gson</artifactId>
      <version>0.12.6</version>
      <scope>runtime</scope>
    </dependency>

However, this test fails:

  @Test
  void testJwkSetParsing() {
    String jwkSetJson =
        "{'keys':[{'kid':'key1','kty':'RSA','alg':'RS256','use':'sig','n':'jki4-Fw66lIy6oHk_YHLReGkdX3QkiizGUQHGeG_xjQUbwlOFejYm-CsMjWEpZcohX0BQVZomnrMCZC_qjNy-Tg5AIFcQZGXehT4kH_DXQZZR4OgT3uKvEEbMEYhZMPj5Bs9--420ONvCLMTU720UXqSF9IrXsuxtRZuaijwkMpQ2t9nIuJ6NKo_CBJHyeVvfLKN3a83Zi-6It6dkiLsOSvhQfbUAsr0NeKAobwmqGt9lT_K_JoLRVqTzFEC-XT7keobMdT9cKba2ML7Yz982Tr5BuGLXZTm7nfPKdk9Bi68HnO82Aas19_D5HJRieW7FqeqwE5MVl6E3IFt8HKblw','e':'AQAB','x5c':['MIICqTCCAZECBgFxOn9tejANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1hY2FkZW1pY2Nsb3VkMB4XDTIwMDQwMjEwNDQyMVoXDTMwMDQwMjEwNDYwMVowGDEWMBQGA1UEAwwNYWNhZGVtaWNjbG91ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI5IuPhcOupSMuqB5P2By0XhpHV90JIosxlEBxnhv8Y0FG8JThXo2JvgrDI1hKWXKIV9AUFWaJp6zAmQv6ozcvk4OQCBXEGRl3oU+JB/w10GWUeDoE97irxBGzBGIWTD4+QbPfvuNtDjbwizE1O9tFF6khfSK17LsbUWbmoo8JDKUNrfZyLiejSqPwgSR8nlb3yyjd2vN2YvuiLenZIi7Dkr4UH21ALK9DXigKG8JqhrfZU/yvyaC0Vak8xRAvl0+5HqGzHU/XCm2tjC+2M/fNk6+Qbhi12U5u53zynZPQYuvB5zvNgGrNffw+RyUYnluxanqsBOTFZehNyBbfBym5cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJ+N/5p1bzHGgBT45cTe23Q3n0j8xogrJKk0LJLmcZTOU2nkcQhZeL4bpw/7eJasgnXCIMk37Ti8xiZKLPhSrRg0BcNIrKmrtA+2x6jDRIc0DoU3P83fM5h4ShUJAW+aKOx7JpV3E0KkOPzHbCRCB2w7oCleZHG9lJAGkcHAQQ01aIfyU3ow66kdAHyB6sAAnRXMf6aogTguqPVB8uAE3VTAWZDiPwyhqo/IVWDMs73bUty8qLDDj4Ei0Q+DcND3WghyeOGm8lCmAzPgl/zzphkQ6P+4Sq1gW06yfVvj862CuY9i6oBTldtAUvjKxCzl+QS8KzQBnB0oUzaFh8vHKYw=='],'x5t':'6yqDA3RjYFZrc3DVcyVrvkNiMA0','x5t#S256':'ifr81ikFJWOiNf2FDgW8dSOu5HEajKuwfGIw78G--Jw'},{'kid':'key2','kty':'RSA','alg':'RS512','use':'sig','n':'kKrdBB_DT37-GT75n_HdOSS0wxXIuheahBdJwTCHmB2Uk3IATjOpiFZjB8qAZ5d00AUu-oZrHG2VgnJq1jUiSb-RDYJTwA1lFXKEJu5CZwAB9xlfCFRXqPs9AL3-2l9-i5ajkMbSE10-S3dacwsrCFd-FL7w0428K7DHjtdwA0mWCyZW6nqWc7lutXhIfFlSmo7GY8M9tuMoOAOXOnLa0MYGh6G1jGvK8pNEyBTnKNEWqAIZhH8ENPMLm4vNFkuenFnP5VzcFNppwt3FnVFetnwudFPfEzUeHqyH7EsdOooFbD4IBu1iWXdI09uGCIJ30BJ2Q-OpXLGMur_YhXMb-Q','e':'AQAB','x5c':['MIICqTCCAZECBgGEexA7YjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1hY2FkZW1pY2Nsb3VkMB4XDTIyMTExNTExMzExMloXDTMyMTExNTExMzI1MlowGDEWMBQGA1UEAwwNYWNhZGVtaWNjbG91ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJCq3QQfw09+/hk++Z/x3TkktMMVyLoXmoQXScEwh5gdlJNyAE4zqYhWYwfKgGeXdNAFLvqGaxxtlYJyatY1Ikm/kQ2CU8ANZRVyhCbuQmcAAfcZXwhUV6j7PQC9/tpffouWo5DG0hNdPkt3WnMLKwhXfhS+8NONvCuwx47XcANJlgsmVup6lnO5brV4SHxZUpqOxmPDPbbjKDgDlzpy2tDGBoehtYxryvKTRMgU5yjRFqgCGYR/BDTzC5uLzRZLnpxZz+Vc3BTaacLdxZ1RXrZ8LnRT3xM1Hh6sh+xLHTqKBWw+CAbtYll3SNPbhgiCd9ASdkPjqVyxjLq/2IVzG/kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQ4kzB7EwhUTGSr/IpAQa+viF7uYGxk+Iiec/s+ShfkFLoMPw+y9l5alwvnKAgTI1Pxrjha8Hu2OsCtGtu8ziJNd65VQiNoFsZp71WGq0+7+Zcqmk182CmjqoN+io7yfgg7N5/VygquHIY3aNB4riruQrbR33fQ49mjpZIM2eohU1teycfpwPCObTRGg5jZg+iUREy01k+QZplxgOqgyqrtTDUKoxZr8WwAWjlCBWyylOT5eEA/777yObYogmfrpNovo+dw1szaHB4BGfX1S522UUfRDAMtOTsjfCnusYEZsMUWXJe46ZLSYJsmIpGw4UwSC7I371elrC/dTzCSREpQ=='],'x5t':'PwDhZrjmydYTXbB3skSdfamw5Xk','x5t#S256':'44mNK5ARKWGn8S7R0tfEeJ6SVqpLp73VSeQeG2wMATE'}]}"
            .replace("'", "\"");
    JwkSet jwkSet = Jwks.setParser().build().parse(jwkSetJson);
    assertNotEquals("keys", String.join("", jwkSet.keySet()));
    assertTrue(jwkSet.containsKey("key1"));
    assertTrue(jwkSet.containsKey("key2"));
    assertEquals(2, jwkSet.keySet().size());
  }

Apparently, it parses the "keys" attribute as the key id and doesn't recognize that the keys to be parsed are in the array.

Am I using the JJWT library in a wrong way? Is my jwkSetJson not RFC7517 compliant? Is this a bug in JJWT?


Solution

    1. The example json is indeed a JWK Set in the sense of RFC7517.

    2. The test in the question is not using the JJWT library in the intended way and confuses "keys" in the JSON sense, "keys" in the Java Map interface sense and "keys" in the JWK Set sense of the term.

    The JwkSet is a direct representation of the json object and as such a Map<String,?>. The containsKey and keySet methods are Map methods. The correct method to iterate over the JWK "Keys" is the JwkSet#getKeys() method (as pointed out by @andrewJames in the comments of the question). The returning iterator can be converted to a List<Jwk<?>>easily. This is a passing test:

    String json =  "{'keys':[{'kid':'key1','kty':'RSA','alg':'RS256','use':'sig','n':'jki4-Fw66lIy6oHk_YHLReGkdX3QkiizGUQHGeG_xjQUbwlOFejYm-CsMjWEpZcohX0BQVZomnrMCZC_qjNy-Tg5AIFcQZGXehT4kH_DXQZZR4OgT3uKvEEbMEYhZMPj5Bs9--420ONvCLMTU720UXqSF9IrXsuxtRZuaijwkMpQ2t9nIuJ6NKo_CBJHyeVvfLKN3a83Zi-6It6dkiLsOSvhQfbUAsr0NeKAobwmqGt9lT_K_JoLRVqTzFEC-XT7keobMdT9cKba2ML7Yz982Tr5BuGLXZTm7nfPKdk9Bi68HnO82Aas19_D5HJRieW7FqeqwE5MVl6E3IFt8HKblw','e':'AQAB','x5c':['MIICqTCCAZECBgFxOn9tejANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1hY2FkZW1pY2Nsb3VkMB4XDTIwMDQwMjEwNDQyMVoXDTMwMDQwMjEwNDYwMVowGDEWMBQGA1UEAwwNYWNhZGVtaWNjbG91ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI5IuPhcOupSMuqB5P2By0XhpHV90JIosxlEBxnhv8Y0FG8JThXo2JvgrDI1hKWXKIV9AUFWaJp6zAmQv6ozcvk4OQCBXEGRl3oU+JB/w10GWUeDoE97irxBGzBGIWTD4+QbPfvuNtDjbwizE1O9tFF6khfSK17LsbUWbmoo8JDKUNrfZyLiejSqPwgSR8nlb3yyjd2vN2YvuiLenZIi7Dkr4UH21ALK9DXigKG8JqhrfZU/yvyaC0Vak8xRAvl0+5HqGzHU/XCm2tjC+2M/fNk6+Qbhi12U5u53zynZPQYuvB5zvNgGrNffw+RyUYnluxanqsBOTFZehNyBbfBym5cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJ+N/5p1bzHGgBT45cTe23Q3n0j8xogrJKk0LJLmcZTOU2nkcQhZeL4bpw/7eJasgnXCIMk37Ti8xiZKLPhSrRg0BcNIrKmrtA+2x6jDRIc0DoU3P83fM5h4ShUJAW+aKOx7JpV3E0KkOPzHbCRCB2w7oCleZHG9lJAGkcHAQQ01aIfyU3ow66kdAHyB6sAAnRXMf6aogTguqPVB8uAE3VTAWZDiPwyhqo/IVWDMs73bUty8qLDDj4Ei0Q+DcND3WghyeOGm8lCmAzPgl/zzphkQ6P+4Sq1gW06yfVvj862CuY9i6oBTldtAUvjKxCzl+QS8KzQBnB0oUzaFh8vHKYw=='],'x5t':'6yqDA3RjYFZrc3DVcyVrvkNiMA0','x5t#S256':'ifr81ikFJWOiNf2FDgW8dSOu5HEajKuwfGIw78G--Jw'},{'kid':'key2','kty':'RSA','alg':'RS512','use':'sig','n':'kKrdBB_DT37-GT75n_HdOSS0wxXIuheahBdJwTCHmB2Uk3IATjOpiFZjB8qAZ5d00AUu-oZrHG2VgnJq1jUiSb-RDYJTwA1lFXKEJu5CZwAB9xlfCFRXqPs9AL3-2l9-i5ajkMbSE10-S3dacwsrCFd-FL7w0428K7DHjtdwA0mWCyZW6nqWc7lutXhIfFlSmo7GY8M9tuMoOAOXOnLa0MYGh6G1jGvK8pNEyBTnKNEWqAIZhH8ENPMLm4vNFkuenFnP5VzcFNppwt3FnVFetnwudFPfEzUeHqyH7EsdOooFbD4IBu1iWXdI09uGCIJ30BJ2Q-OpXLGMur_YhXMb-Q','e':'AQAB','x5c':['MIICqTCCAZECBgGEexA7YjANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1hY2FkZW1pY2Nsb3VkMB4XDTIyMTExNTExMzExMloXDTMyMTExNTExMzI1MlowGDEWMBQGA1UEAwwNYWNhZGVtaWNjbG91ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJCq3QQfw09+/hk++Z/x3TkktMMVyLoXmoQXScEwh5gdlJNyAE4zqYhWYwfKgGeXdNAFLvqGaxxtlYJyatY1Ikm/kQ2CU8ANZRVyhCbuQmcAAfcZXwhUV6j7PQC9/tpffouWo5DG0hNdPkt3WnMLKwhXfhS+8NONvCuwx47XcANJlgsmVup6lnO5brV4SHxZUpqOxmPDPbbjKDgDlzpy2tDGBoehtYxryvKTRMgU5yjRFqgCGYR/BDTzC5uLzRZLnpxZz+Vc3BTaacLdxZ1RXrZ8LnRT3xM1Hh6sh+xLHTqKBWw+CAbtYll3SNPbhgiCd9ASdkPjqVyxjLq/2IVzG/kCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAQ4kzB7EwhUTGSr/IpAQa+viF7uYGxk+Iiec/s+ShfkFLoMPw+y9l5alwvnKAgTI1Pxrjha8Hu2OsCtGtu8ziJNd65VQiNoFsZp71WGq0+7+Zcqmk182CmjqoN+io7yfgg7N5/VygquHIY3aNB4riruQrbR33fQ49mjpZIM2eohU1teycfpwPCObTRGg5jZg+iUREy01k+QZplxgOqgyqrtTDUKoxZr8WwAWjlCBWyylOT5eEA/777yObYogmfrpNovo+dw1szaHB4BGfX1S522UUfRDAMtOTsjfCnusYEZsMUWXJe46ZLSYJsmIpGw4UwSC7I371elrC/dTzCSREpQ=='],'x5t':'PwDhZrjmydYTXbB3skSdfamw5Xk','x5t#S256':'44mNK5ARKWGn8S7R0tfEeJ6SVqpLp73VSeQeG2wMATE'}]}"
                    .replace("'", "\"");
    JwkSet jwks = Jwks.setParser().build().parse(json);
    assert "keys".equals(String.join("", jwks.keySet()));
    List<Jwk<?>> keys = jwks.getKeys().stream().toList();
    assert keys.get(0).getId().equals("key1");
    assert keys.get(1).getId().equals("key2");
    assert 2 == keys.size();
    

    Source: lhazlewood's comment in the JJWT repository Discussion#1002