Serhii
Serhii

Reputation: 7563

How to produce correct jwks endpoint for spring oauth2 jwt server?

The Issued Goal

To configure /.well-known/jwks.json for my spring oauth2 jwt server with valid jwks.

1st Attempt

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.

2nd Attempt

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"}

3rd Attempt

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"}

Question

Is any ideas or examples how to configure /.well-known/jwks.json to produce correct jwks?

P.S.

Upvotes: 2

Views: 4416

Answers (2)

Luke 10X
Luke 10X

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

Dean Andrews
Dean Andrews

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

Related Questions