13

I am trying to read a Json Web Token(JWT) generated from Google OpenID Connect's Id Token in order to get the claims and to verify using jjwt library. I have tried several ways to fix it with the code below.


 String publicKeyFromJsonFile = "-----BEGIN PUBLIC KEY-----xxxxxxx-----END PUBLIC KEY-----"

 Claims claims = Jwts.parser()
                .setSigningKey(publicKeyFromJsonFile)
                .parseClaimsJws(jwt).getBody();

 System.out.println(claims);


but I am getting this error:

java.lang.IllegalArgumentException: Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance

Please what could be the right approach to follow ?

John Erbynn
  • 355
  • 1
  • 4
  • 14
  • 1
    Looks like you will have to parse your public key using `KeyFactory` and then pass the resulting instance to your library. – Michał Krzywański Feb 03 '20 at 12:25
  • 1
    hey @michalk .... I ended up fixing the **publicKey** with this code snippet ------------------- `X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(PUBLIC_KEY));` ________ `RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(keySpecX509);` – John Erbynn Feb 03 '20 at 23:54
  • It worked fine with that snippet temporary. It's now giving different error. that is, `JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted` ....... I don't know if that was what you were actually recommending. If not, could you help me out. Thanks. – John Erbynn Feb 04 '20 at 00:01

4 Answers4

4

I think I have been able to fix the gotcha by parsing the publicKey as an RSAPublicKey. Below is the nitty-gritty of how i went about it.

 public static Optional<RSAPublicKey> getParsedPublicKey(){
       // public key content...excluding '---PUBLIC KEY---' and '---END PUBLIC KEY---'
        String PUB_KEY =System.getenv("PUBLIC_KEY") ; 

       // removes white spaces or char 20
        String PUBLIC_KEY = "";
          if (!PUB_KEY.isEmpty()) {
            PUBLIC_KEY = PUB_KEY.replace(" ", "");
        }

        try {
            byte[] decode = com.google.api.client.util.Base64.decodeBase64(PUBLIC_KEY);
            X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(decode);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            RSAPublicKey pubKey = (RSAPublicKey) keyFactory.generatePublic(keySpecX509);
            return Optional.of(pubKey);

        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
            System.out.println("Exception block | Public key parsing error ");
            return Optional.empty();
        }

Hope it helps :).

John Erbynn
  • 355
  • 1
  • 4
  • 14
3

This can be done like below, generate a token using private key signing and parse claims with the public key

  @Configuration
  public class KeyGeneratorConfig {


    @Value("${jwt.privateKey}")
    private String privateKey; //Encoded private key string


    @Value("${jwt.publicKey}")
    private String publicKey;//Encoded public key string


    @Bean
    public PrivateKey generatePrivateKey() throws NoSuchAlgorithmException, InvalidKeySpecException {

        KeyFactory kf = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec privKeySpecPKCS8 = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));
        return kf.generatePrivate(privKeySpecPKCS8);
    }

    @Bean
    public PublicKey generatePublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException {

        KeyFactory kf = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec pubKeySpecX509EncodedKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));
        return kf.generatePublic(pubKeySpecX509EncodedKeySpec);
    }
}

and generating token and parsing can be done like this

@Autowired
private PublicKey publicKey;

@Autowired
private PrivateKey privateKey;

private String doGenerateToken(Map claims) {
    return Jwts.builder()
            .setClaims(claims)
            .setExpiration(generateExpirationDate("token"))
            .signWith(SignatureAlgorithm.RS512, privateKey)
            .compact();
}

public Claims getClaimsFromToken(String token) throws ExpiredJwtException, UnsupportedJwtException,
        MalformedJwtException, SignatureException, IllegalArgumentException {
    Claims claims;

    claims = Jwts.parser()
            .setSigningKey(publicKey)
            .parseClaimsJws(token)
            .getBody();

    return claims;
}
Suresh Kb
  • 151
  • 1
  • 8
1

A good approach can be to use the JWT.IO web page to validate the token manually - as described in my article - then to apply the equivalent code - though my code is NodeJS.

Interested in how you come to be validating an id token manually - could you explain which clients and APIs are involved - there may be a more standard way to achieve your goals.

Gary Archer
  • 22,534
  • 2
  • 12
  • 24
  • Alright. So I am generating the tokenID with Google OpenID approach at the client side with Angular and passing the token down to the server side which is in Java(specifically Spring MVC) for verification using [jjwt](https://github.com/jwtk/jjwt#verification-key) and sending response back to the client before and micro-service can be accessed if token is actually valid. Hope you get it :) – John Erbynn Feb 04 '20 at 00:10
  • Ok - sounds standard except it is usual to send access tokens to the API rather than id tokens, since access tokens are short lived API credentials that can be renewed. Your tech approach to JWT validation looks fine otherwise. – Gary Archer Feb 04 '20 at 08:21
1

My relive came with this post.

import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
...
// First, get your PEM-encoded DER certificate into a String, like this:
String certChart = "-----BEGIN CERTIFICATE-----\n.....";

// Now parse it using CertificateFactory:
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate cert = cf.generateCertificate(new java.io.ByteArrayInputStream(certChars.getBytes(StandardCharsets.US_ASCII)));

// Now verify:
Jwts.parserBuilder()
    .setSigningKey(cert.getPublicKey())
 ...
Camille
  • 2,439
  • 1
  • 14
  • 32