I am looking to implement the accepted answer, which is generating my own RSA key pair to use within the unit tests. I have written the following method to create the key pair:
private void generateKeyPair(String targetDir) throws Exception {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
PublicKey pub = keyPair.getPublic();
PrivateKey pvt = keyPair.getPrivate();
String outFile = targetDir + "testKey";
OutputStream out = new FileOutputStream(outFile + ".key");
out.write(pvt.getEncoded());
out.close();
out = new FileOutputStream(outFile + ".pub");
out.write(pub.getEncoded());
out.close();
}
Currently, I am writing this out to target
directory within the project. I am then using the following code to generate JWKS file given the previously generated private key:
private void createJwk(String kid, String use, String targetDir) {
JSONObject json = new JSONObject();
try {
//byte[] keyBytes = Files.readAllBytes(Paths.get("./private_key.der"));
byte[] keyBytes = Files.readAllBytes(Paths.get(targetDir + "testKey.key"));
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey privateKey = kf.generatePrivate(spec);
RSAPrivateCrtKey rsaPrivateKey = (RSAPrivateCrtKey) privateKey;
BigInteger mod = rsaPrivateKey.getModulus();
BigInteger pubExp = rsaPrivateKey.getPublicExponent();
BigInteger prvExp = rsaPrivateKey.getPrivateExponent();
BigInteger primeP = rsaPrivateKey.getPrimeP();
BigInteger primeExponentP = rsaPrivateKey.getPrimeExponentP();
BigInteger primeQ = rsaPrivateKey.getPrimeQ();
BigInteger primeExponentQ = rsaPrivateKey.getPrimeExponentQ();
BigInteger crtCoefficient = rsaPrivateKey.getCrtCoefficient();
java.util.Base64.Encoder encoder = java.util.Base64.getUrlEncoder().withoutPadding();
String n = encoder.encodeToString(mod.toByteArray());
String e = encoder.encodeToString(pubExp.toByteArray());
String d = encoder.encodeToString(prvExp.toByteArray());
String p = encoder.encodeToString(primeP.toByteArray());
String q = encoder.encodeToString(primeQ.toByteArray());
String dp = encoder.encodeToString(primeExponentP.toByteArray());
String dq = encoder.encodeToString(primeExponentQ.toByteArray());
String qi = encoder.encodeToString(crtCoefficient.toByteArray());
json.put("n", n);
json.put("e", e);
json.put("d", d);
json.put("p", p);
json.put("q", q);
json.put("dp", dp);
json.put("dq", dq);
json.put("qi", qi);
json.put("alg", "RS256");
json.put("kty", "RSA");
json.put("kid", kid);
json.put("use", use);
try (FileWriter file = new FileWriter(targetDir + "jwks.json")) {
file.write(json.toJSONString());
file.flush();
} catch (IOException exp) {
exp.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
The issue I face is when it comes to creating the JWT, and signing it. I am using Auth0 library for JWT implementation:
RSAKeyProvider keyProvider = rsaUtils.getRSAKeyProvider(keyPath);
Date exp;
try {
Algorithm algo = Algorithm.RSA256(keyProvider);
Calendar c = Calendar.getInstance();
c.setTime(issuedAt);
c.add(Calendar.MONTH, ttl);
exp = c.getTime();
String token = JWT.create()
.withIssuer(issuer)
.withAudience(audience)
.withSubject(subject)
.withIssuedAt(issuedAt)
.withExpiresAt(exp)
.withJWTId(jwtId)
.withArrayClaim("products", someProducts)
.sign(algo);
...
}
In the above, the value of keyPath
refers to the target
dir containing the previously generated keys. On drilling down into the code, it fails when it comes to trying to retrieve the private key from the JWKS, this happens when calling sign(algo)
as seen in code above. The code in question sits in com.auth0.jwt.algorithms.RSAAlgorithm
class:
public byte[] sign(byte[] headerBytes, byte[] payloadBytes) throws SignatureGenerationException {
try {
RSAPrivateKey privateKey = (RSAPrivateKey)this.keyProvider.getPrivateKey(); // here it is null
if (privateKey == null) {
throw new IllegalStateException("The given Private Key is null.");
} else {
return this.crypto.createSignatureFor(this.getDescription(), privateKey, headerBytes, payloadBytes);
}
} catch (SignatureException | InvalidKeyException | IllegalStateException | NoSuchAlgorithmException var4) {
throw new SignatureGenerationException(this, var4);
}
}
Keen to see whether this can be resolved in a programmatical way. One thing that strikes me is that the generated keys might not be in the correct format. I have seen references online to keys needing to be in .DER format, not JKS, in order for them to correctly generate a JWKS.json file from them. If so, how can I amend the original key generation code to make it .DER format in a programmatical way? Or are there any other suggestions to be able to resolve my issue using the format that the keys were already generated in?
Many thanks
I came up with the following to be able to use within test cases:
public static String generateAndSignTestJwt() throws Exception {
String[] productsList = Arrays.asList("aProduct", "anotherProduct")
.stream()
.map(Object::toString)
.toArray(String[]::new);
LocalDate localDate = LocalDate.now();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
Algorithm algorithm = Algorithm.RSA256((
RSAPublicKey) keyPair.getPublic(), (RSAPrivateKey) keyPair.getPrivate());
return JWT.create()
.withExpiresAt(Date.from(LocalDateTime.now().plusSeconds(120).atZone(ZoneId.systemDefault()).toInstant()))
.withIssuer("testIssuer1")
.withAudience("testAudience1")
.withSubject("testSubject1")
.withIssuedAt(Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
.withJWTId("testing-token-123")
.withArrayClaim("products", productsList)
.sign(algorithm);
}
This generates a key pair on the fly to use, which then signs the JWT. Within my test cases I then use Mockito to pull in the test JWT generation method (generateAndSignTestJwt()
) when the actual implementation (generateAndSignJwt()
) is invoked...
JWTUtils jwtUtilsMock = mock(JWTUtils.class);
when(jwtUtilsMock.generateAndSignJwt(any(), anyString(), anyString(), anyString(), any(), anyInt(), anyString())).thenReturn(generateAndSignTestJwt());
...
So, there is no need for a path to the keys (keyPath
), and so works perfectly in my unit tests and when running the build in GitLab.