Reputation: 7563
To configure /.well-known/jwks.json
for my spring oauth2 jwt server with valid jwks
.
Following spring documentation I can use out the box Endpoint for JWK Set URI. It requires:
@Import(AuthorizationServerEndpointsConfiguration.class)
I've added. Checking mapped endpoints via actuator nothing filtered for jw
.
Following the same configuration I tried to use next code:
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
...
@FrameworkEndpoint
class JwkSetEndpoint {
KeyPair keyPair;
public JwkSetEndpoint(KeyPair keyPair) {
this.keyPair = keyPair;
}
@GetMapping("/.well-known/jwks.json")
@ResponseBody
public Map<String, Object> getKey(Principal principal) {
RSAPublicKey publicKey = (RSAPublicKey) this.keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
}
It produces
{
"keys" : [ {
"kty" : "RSA",
"e" : "AQAB",
"n" : "mWI2jtKwvf0W1hdMdajch-mFx9FZe3CZnKNvT_d0-2O6V1Pgkz7L2FcQx2uoV7gHgk5mmb2MZUsy_rDKj0dMfLzyXqBcCRxD6avALwu8AAiGRxe2dl8HqIHyo7P4R1nUaea1WCZB_i7AxZNAQtcCcSvMvF2t33p3vYXY6SqMucMD4yHOTXexoWhzwRqjyyC8I8uCYJ-xIfQvaK9Q1RzKRj99IRa1qyNgdeHjkwW9v2Fd4O_Ln1Tzfnk_dMLqxaNsXPw37nw-OUhycFDPPQF_H4Q4-UDJ3ATf5Z2yQKkUQlD45OO2mIXjkWprAmOCi76dLB2yzhCX_plGJwcgb8XHEQ"
} ]
}
Pinging resource server with access_token is failed:
{"error":"invalid_token","error_description":"Invalid JWT/JWS: kid is a required JOSE Header"}
Modifying response for "/.well-known/jwks.json"
(jwt.io helps detect algorithm used for jwt
):
RSAKey key = new RSAKey.Builder(publicKey)
.keyID("1")
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.build();
leads to next response:
{
"keys" : [ {
"kty" : "RSA",
"e" : "AQAB",
"use" : "sig",
"kid" : "1",
"alg" : "RS256",
"n" : "mWI2jtKwvf0W1hdMdajch-mFx9FZe3CZnKNvT_d0-2O6V1Pgkz7L2FcQx2uoV7gHgk5mmb2MZUsy_rDKj0dMfLzyXqBcCRxD6avALwu8AAiGRxe2dl8HqIHyo7P4R1nUaea1WCZB_i7AxZNAQtcCcSvMvF2t33p3vYXY6SqMucMD4yHOTXexoWhzwRqjyyC8I8uCYJ-xIfQvaK9Q1RzKRj99IRa1qyNgdeHjkwW9v2Fd4O_Ln1Tzfnk_dMLqxaNsXPw37nw-OUhycFDPPQF_H4Q4-UDJ3ATf5Z2yQKkUQlD45OO2mIXjkWprAmOCi76dLB2yzhCX_plGJwcgb8XHEQ"
} ]
}
Pinging resource server with access_token provides the same result:
{"error":"invalid_token","error_description":"Invalid JWT/JWS: kid is a required JOSE Header"}
Is any ideas or examples how to configure /.well-known/jwks.json
to produce correct jwks
?
jwks
libraries that can be used in spring-boot application).Upvotes: 2
Views: 4416
Reputation: 1420
To generate JWKS endpoint you could use some good library like nimbus-jose-jwt, but it is also possible to do it with no external libraries at all.
For that you need to generate the key file:
ssh-keygen -t rsa -b 4096 -m PEM -f rs256.key
and convert to what Java can work with:
openssl pkcs8 -topk8 -inform PEM -in rs256.key -out rs256.pem -nocrypt
The JWKS endpoint response then can be generated with the following code:
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
class JwksEndpoint {
public static void main(String[] args) {
// *******************************
// Load private key data from file
// *******************************
String filePath = "rs256.pem";
String privateKeyContent;
try {
privateKeyContent = new String(Files.readAllBytes(Paths.get(filePath)))
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("\n", "");
} catch (IOException e) {
throw new RuntimeException(e);
}
// ******************
// Create private key
// ******************
KeyFactory keyFactory;
try {
keyFactory = KeyFactory.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(
Base64.getDecoder().decode(privateKeyContent)
);
PrivateKey privateKey; // Use this key also to sign JWT!
try {
privateKey = keyFactory.generatePrivate(privateKeySpec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
// *****************
// Create public key
// *****************
RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(
((RSAPrivateCrtKey) privateKey).getModulus(),
((RSAPrivateCrtKey) privateKey).getPublicExponent()
);
PublicKey publicKey;
try {
publicKey = keyFactory.generatePublic(publicKeySpec);
} catch (InvalidKeySpecException e) {
throw new RuntimeException(e);
}
// *********************
// Prepare JWKS response
// *********************
RSAPublicKey rsa = (RSAPublicKey) publicKey;
Map<String, Object> keyObject = new HashMap<>();
keyObject.put("kty", rsa.getAlgorithm());
keyObject.put("kid", "1");
keyObject.put(
"n",
Base64.getUrlEncoder().encodeToString(rsa.getModulus().toByteArray())
);
keyObject.put(
"e",
Base64.getUrlEncoder().encodeToString(rsa.getPublicExponent().toByteArray())
);
keyObject.put("alg", "RS256");
keyObject.put("use", "sig");
List<Map<String, Object>> keyList = new ArrayList<>();
keyList.add(keyObject);
Map<String, List<Map<String, Object>>> jwksResponse = new HashMap<>();
jwksResponse.put("keys", keyList);
System.out.println(jwksResponse);
}
}
You probably need some library like org.json
or Jackson to serialize the JWKS data to JSON unless you want to use StringBuilder. Still, the generation of JWKS itself does not need any external libraries.
Upvotes: 1
Reputation: 31
Any update to this?
We are looking into signature based authentication, https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12,
One of the requirements will be to implement the /.well-known/jwks.json endpoint so we don't have to have a separate public key distribution mechanism.
I haven't yet done this but it looks like:
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
return JWKSet.load(keyStore, (name) -> null).toPublicJWKSet();
Upvotes: 1