Reputation: 610
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.
Upvotes: 4
Views: 6836
Reputation: 656
According to a developer of the package here, a JWK set, not individual JWK, should be exposed through the endpoint (see https://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens):
Publishing a JWK set (JSON array) of multiple keys, instead of just one key at the URL is intended to facilitate smooth key roll-over. With a single key being published switching to a new key can lead to errors on the client side.
Anyway, if you want to keep the one-key-per URL, I suggest you override the existing
com.nimbusds.jose.jwk.source.RemoteJWKSet
, or implement your own single JWK source withcom.nimbusds.jose.jwk.source.JWKSource
(possibly copying code where needed fromRemoteJWKSet
).
UPDATE: Just found out that the same serialization issue in the question exists for JWKSet
too 😔
Upvotes: 0
Reputation: 610
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.
Upvotes: 2