2

I'm trying to verify a json web token generated by jose4j using jsonwebtoken in node.js and I see the following error:

[Error: PEM_read_bio_PUBKEY failed]

The jose4j code is basically lifted straight from the example:

RsaJsonWebKey key = RsaJwkGenerator.generateJwk(2048);
key.setKeyId("global.authenticated");

byte[] raw = key.getKey().getEncoded();
Base64.Encoder encoder = Base64.getEncoder();
System.out.printf("Public Key [%s]\n", encoder.encodeToString(raw));

JwtClaims claims = new JwtClaims();
claims.setIssuer("global.gen");
claims.setAudience("global.cons");
claims.setExpirationTimeMinutesInTheFuture(12 * 60);
claims.setGeneratedJwtId();
claims.setIssuedAtToNow();
claims.setNotBeforeMinutesInThePast(2);
claims.setSubject("nim");
claims.setClaim("role", "tester");

JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setKey(key.getPrivateKey());
jws.setKeyIdHeaderValue(key.getKeyId());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
String token = jws.getCompactSerialization();
System.out.printf("Generated Token [%s]\n", token);


JwtConsumer jwtConsumer = new JwtConsumerBuilder()
    .setRequireExpirationTime() // the JWT must have an expiration time
    .setAllowedClockSkewInSeconds(30) // allow some leeway in validating time based claims to account for clock skew
    .setRequireSubject() // the JWT must have a subject claim
    .setExpectedIssuer("global.gen") // whom the JWT needs to have been issued by
    .setExpectedAudience("global.cons") // to whom the JWT is intended for
    .setVerificationKey(key.getKey()) // verify the signature with the public key
    .build(); // create the JwtConsumer instance

try {
  //  Validate the JWT and process it to the Claims
  JwtClaims jwtClaims = jwtConsumer.processToClaims(token);
  System.out.println("JWT validation succeeded! " + jwtClaims);
} catch (InvalidJwtException e) {
  // InvalidJwtException will be thrown, if the JWT failed processing or validation in anyway.
  // Hopefully with meaningful explanations(s) about what went wrong.
  System.out.println("Invalid JWT! " + e);
}

So internally the token validates fine. However when I copy the token and the key (for example below is from a run), the above error is reported:

var jwt        = require('jsonwebtoken'); // used to create, sign, and verify tokens

var key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkRWAQ0O9LgBoHNAB5m1X8e1sPTzKmBTPCFTSRTzw0AjZozIbN4nIp/3jnQHTbcY0Bf5MDWmtdheSK1a+ew34YcgN2b9Shr+3yZv9PJ97i7gRCqOnI7jbm7PXFBNw1I4aMYc6tV7TKFzvx6008/nYvN3Jey6Z8ItS/FLQRDkV9m/WQkhJpYgvmD6qiwj9d+un+moBQ5/PPgn7Qkg5GyxZUy9PsblUDSrIA0bEiv/wQOXCYUvL9OFzxTUSeIHpdGibhPQVxX3Jnpr293Iq/mOKn3ZO+xBID26m3L8+ik64wte041y1S4HHaE9Q082ai/uBduAwIHcJY5VAHborZYCSaQIDAQAB';
var token = 'eyJraWQiOiJnbG9iYWwuYXV0aGVudGljYXRlZCIsImFsZyI6IlJTMjU2In0.eyJpc3MiOiJnbG9iYWwuZ2VuIiwiYXVkIjoiZ2xvYmFsLmNvbnMiLCJleHAiOjE0NDMwNjMyMDgsImp0aSI6InpweF9ERW8tX1h2Q1hnZmNZTUpiZ0EiLCJpYXQiOjE0NDMwMjAwMDgsIm5iZiI6MTQ0MzAxOTg4OCwic3ViIjoibmltIiwicm9sZSI6InRlc3RlciJ9.inEebSQ8jYPQsTpHnvw-gMpoNbJl5ErUkS8FtkDagWrwijUgG8XYYP8FLi2ZCpdgDqUsP6nE1iG0_2wWuL7B7C7wUpZlrqR2bEOG2cXK9s26VqNAXu8I7BTDaZBKmdOt1aFVWozGsN8iUCsQ7Yt9-GfvNRP1yeOoMgpOxf_wVa0QVzsV18aVi_oSeiMqOkQ_6n7JOjFVdiURm0ew4vh5TBaMcEcS35a9jtPxuFR_Z_FaLUk0g06PDVKcdsK1-FYRAGBlRGDkea8Hs9Zh-ZIxgcs2QfWzq5PSsIKum1dWqNLW04ullWmlbAO-5d0V0NAnkh4FFoi3N7AedvkILJgbqA';

jwt.verify(token, key, { algorithms: ['RS256'] }, function(err, decoded) {
  if (err)
    console.log(err);
  else
    console.log(decoded);
});

Is there some bit of magic that I'm missing (either in jose4j or jsonwebtoken) that will allow the generated token to be validated by the public key?

On a side note, pasting the token into jwt.io decodes the header and payload correctly, however the signature fails to be verified using the same public key. I'm guessing the problem really is in the jose4j side - but not sure.

Nim
  • 33,299
  • 2
  • 62
  • 101

2 Answers2

6

I'll add this as an answer incase someone hits something similiar. The problem stems from the format of the key passed into jsonwebtoken. It can't just be a plain text public key, it has to follow the PEM format specifically. So once I convert the key to a PEM file, specficially by making the following changes:

byte[] raw = key.getKey().getEncoded();
Base64.Encoder encoder = Base64.getMimeEncoder(64, new byte[]{'\n'});

And then wrapping with

-----BEGIN PUBLIC KEY-----
<Key>
-----END PUBLIC KEY-----

Then passing the resulting file into jsonwebtoken allows the authentication to proceed. I didn't realize that the certificate passed into the validation step should be formatted so strictly (i.e. with the line sizes and the wrappers!)

NOTE: the bouncycastle package has a PemObject class and a PemWriter which should make writing the file somewhat easier - however I didn't want to pull in another package just for that. Maybe the maintainers of jose4j can add a little class for that to their package..

Nim
  • 33,299
  • 2
  • 62
  • 101
  • I maintain jose4j and, while PEM formatting is really beyond the scope of the library, I will consider adding some simple utilities for specific things like getting the PEM encoded public key. It'd be good also to encourage the auth0 folks to support [JWK](http://tools.ietf.org/html/rfc7517), which is a nice JSON based way of encoding keys. – Brian Campbell Sep 24 '15 at 13:22
  • Thanks! And I just put this in to track the PEM encoding feature https://bitbucket.org/b_c/jose4j/issues/37/ – Brian Campbell Sep 24 '15 at 14:06
  • The next release will have a `KeyPairUtil.pemEncode(PublicKey publicKey)` helper method that will do this. Also decoding. – Brian Campbell Nov 11 '15 at 23:11
  • 2
    jose4j v0.5.0 was recently released and has `KeyPairUtil.pemEncode(PublicKey publicKey)` as well as `fromPemEncoded(String pem)` that will help convert PublicKey objects to/from PEM encoded keys. – Brian Campbell Mar 07 '16 at 22:31
3

It seems as if jsonwebtoken is very strict not only about the -----BEGIN PUBLIC KEY----- header and footer, but also about the line breaks.

A key like

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkRWAQ0O9LgBoHNAB5m1X8e1sPTzKmBTPCFTSRTzw0AjZozIbN4nIp/3jnQHTbcY0Bf5MDWmtdheSK1a+ew34YcgN2b9Shr+3yZv9PJ97i7gRCqOnI7jbm7PXFBNw1I4aMYc6tV7TKFzvx6008/nYvN3Jey6Z8ItS/FLQRDkV9m/WQkhJpYgvmD6qiwj9d+un+moBQ5/PPgn7Qkg5GyxZUy9PsblUDSrIA0bEiv/wQOXCYUvL9OFzxTUSeIHpdGibhPQVxX3Jnpr293Iq/mOKn3ZO+xBID26m3L8+ik64wte041y1S4HHaE9Q082ai/uBduAwIHcJY5VAHborZYCSaQIDAQAB

has to look like this for it to work with jsonwebtoken (mind the header, footer and line breaks):

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkRWAQ0O9LgBoHNAB5m1X
8e1sPTzKmBTPCFTSRTzw0AjZozIbN4nIp/3jnQHTbcY0Bf5MDWmtdheSK1a+ew34
YcgN2b9Shr+3yZv9PJ97i7gRCqOnI7jbm7PXFBNw1I4aMYc6tV7TKFzvx6008/nY
vN3Jey6Z8ItS/FLQRDkV9m/WQkhJpYgvmD6qiwj9d+un+moBQ5/PPgn7Qkg5GyxZ
Uy9PsblUDSrIA0bEiv/wQOXCYUvL9OFzxTUSeIHpdGibhPQVxX3Jnpr293Iq/mOK
n3ZO+xBID26m3L8+ik64wte041y1S4HHaE9Q082ai/uBduAwIHcJY5VAHborZYCS
aQIDAQAB
-----END PUBLIC KEY-----

This function does the job for me:

function base64toPem(base64)
{
    for(var result="", lines=0;result.length-lines < base64.length;lines++) {
        result+=base64.substr(result.length-lines,64)+"\n"
    }

    return "-----BEGIN PUBLIC KEY-----\n" + result + "-----END PUBLIC KEY-----";
}
AndreasPizsa
  • 1,736
  • 19
  • 26