Skip to content

Unable to parse JWK in Java

I implemented a rest authorization server that returns the public-key for a given keyId in the JWK format using the com.nimbusds:nimbus-jose-jwt:9.13 package. The code looks something like this:

@RequestMapping(value = "/oauth2", produces = APPLICATION_JSON_VALUE)
public interface Rest {
...
    @GetMapping("/public-key/{keyId}")
    @Operation(summary = "Return the public key corresponding to the key id")
    JWK getPublicKey(@PathVariable String keyId);
}

public class RestController implements Rest {
.....
 public JWK getPublicKey(String keyId) {
         byte[] publicKeyBytes = ....
         RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
         JWK jwk = new RSAKey.Builder(publicKey)
                .keyID(keyId)
                .algorithm(new Algorithm(publicKey.getAlgorithm()))
                .keyUse(KeyUse.SIGNATURE)
                .build();
         return jwk;
    }
}

This code returns a JWK key in the following format:

{
    "keyStore": null,
    "private": false,
    "publicExponent": {},
    "modulus": {},
    "firstPrimeFactor": null,
    "secondPrimeFactor": null,
    "firstFactorCRTExponent": null,
    "secondFactorCRTExponent": null,
    "firstCRTCoefficient": null,
    "otherPrimes": [],
    "requiredParams": {
        "e": "some-valid-exponent",
        "kty": "RSA",
        "n": "some-valid-modulus"
    },
    "privateExponent": null,
    "x509CertChain": null,
    "algorithm": {
        "name": "RSA",
        "requirement": null
    },
    "keyOperations": null,
    "keyID": "some-valid-key-id",
    "x509CertURL": null,
    "x509CertThumbprint": null,
    "x509CertSHA256Thumbprint": null,
    "parsedX509CertChain": null,
    "keyUse": {
        "value": "sig"
    },
    "keyType": {
        "value": "RSA",
        "requirement": "REQUIRED"
    }
}

On the client side (java), I try to parse the jwk with the following code:

public JWK getPublicKey(String keyId) {
 String json = restTemplate.getForObject(publicUrl + "/oauth2/public-key/" + keyId, String.class);
        try {
            return JWK.parse(json);
        } catch (ParseException e) {
            log.error("Unable to parse JWK", e);
            return null;
        }
}

However, the client is unable to parse the key since parse throws an exception (Missing parameter "kty"). I see that JWK.parse requires a kty key in main JWT josn body, while the default serialization of JWK embeds the kty key within requiredParams key. When I try jwk.toString(), I do see the kty key in the main json body.

Why doesn’t serialization/deserialization of the native JWK object work in a straight-forward manner? What would be the best way to fix this without implementing a custom jwt structure or a serializer/deserializer?

Update 1: This code will work if we change the return type from JWK to Map<String, Object> or String and handle deserialization on the client-side. However, it would be better if the package natively does the (de)serialization for us.

Answer

The answer is to use String for (de)serialization for those facing this problem. Why, you ask? According to the RFC, JWK is a string in the JSON format. While nimbusds:nimbus-jose-jwt defines a JWK object, any APIs that return valid JWK (or JWKSet) can assume that it’s a string.

I also raised this issue with the developers of this package, and they recommended using String or Map<String, Object> for (de)serialization.