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?
The example json is indeed a JWK Set in the sense of RFC7517.
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