Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for JWK #43

Closed
nschwertner opened this issue Dec 24, 2014 · 15 comments
Closed

Support for JWK #43

nschwertner opened this issue Dec 24, 2014 · 15 comments

Comments

@nschwertner
Copy link

I am wondering, if there are any plans to support signature validation with JSON Web Key:
https://tools.ietf.org/html/draft-ietf-jose-json-web-key-38

Here is an example JWK:
https://authorize.smartplatforms.org/jwk

@abalmos
Copy link
Contributor

abalmos commented Feb 14, 2015

brianloveswords/node-jws does the actual signing. @awlayton made a node module that injects jwk support into node-jws (and therefore) node-jsonwebtoken.

https://github.com/OADA/node-jws-jwk

Maybe node-jws should incorporate @awlayton's work upstream.

@kara-ryli
Copy link

Brightspace/node-jwk-to-pem looks like a nice solution for JWK support.

@awlayton
Copy link
Contributor

I ended up using dannycoates/pem-jwk to do JWK with jsonwebtoken.

@omsmith
Copy link

omsmith commented Jun 30, 2015

@RCanine Yep, using it exactly for this use-case, as suggested by the example. Might also check out https://github.com/Brightspace/node-jwk-allowed-algorithms for a better solution than the toString checks jsonwebtoken has to do since the context a JWK provides isn't available.

@daviskoh
Copy link

Are there actual plans to support this? The issue has been open for years and looks quite stale...

@ziluvatar
Copy link
Contributor

This PR could open the door for this, since you would have an async way to load the secret/key based on headers (kid) & payload (issuer).

@daviskoh
Copy link

are there any updates on this? this issue has been open for 4 yrs now 😅

@JacoKoster
Copy link
Contributor

I have been working to make JWK a thing and with the introduction of #480 this should be a breeze. Especially if you combine it with either https://www.npmjs.com/package/jwks-rsa or https://www.npmjs.com/package/jwks-client :-)

@ziluvatar : I think this can be closed :-)

@ziluvatar
Copy link
Contributor

As @JacoKoster said this can be implemented using v8.3.0+ passing a function as secretOrPublicKey parameter (see README for more info)

@coolaj86
Copy link

coolaj86 commented Jan 28, 2019

I'm a few years late to the discussion, but I was just searching to figure out how to do this myself, as I was expecting that jsonwebtoken would support jwks out-of-the-box... momentarily forgetting that node doesn't have JWK support... duh. :)

Then I started doing research and looking into things based on the comments here and I figure I'll just sum up a few things.

First, I'm a bit of an odd duck in that I work a lot on small devices (and I'm old school), so writing vanilla js and reducing dependencies is more important to me than to most people. I've also become even more dependency-phobic in the wake of the malicious code that was included with npm's most popular projects this past year.

JWK-to-PEM (RSA, ECDSA)

Although there are many libraries out there that do JWK-to-PEM and PEM-to-JWK, they're kinda half-baked (i.e. only do RSA or only do JWK-to-PEM or only public keys) and have huge dependency chains. Because of that I dug into node's new RSA and ECDSA APIs (added mid-v10) a while back and created tiny, lightweight libs that to do this sort of thing in just a few kilobytes, rather than megabytes (due to node's native APIs and barebones ASN.1 handling):

Example:

  // barebones dependencies
  var JWT = require('jsonwebtoken');
  var Eckles = require('eckles'); // EC support
  var Rasha = require('rasha'); // RSA support
  // A wrapper that handles JWKs
  function verifyWithJwk(jwt, jwk) {
    var pem;

    if ('EC' === jwk.kty) {
      pem = Eckles.exportSync({ jwk: jwk });
    } else if ('RSA' === jwk.kty) {
      pem = Rasha.exportSync({ jwk: jwk });
    } else {
      throw new Error("Expected EC or RSA key but got '" + jwk.kty + "'");
    }

    return JWT.verify(jwt, pem);
  }
  // Usage example
  try {
    verifyWithJwk(jwt, jwk);
  } catch(e) {
    throw e;
  }

Example w/ Express:

  var JWT = require('jsonwebtoken');
  var Eckles = require('eckles'); // EC support
  var Rasha = require('rasha'); // RSA support
  var request = require('@coolaj86/urequest'); // 0-dependency API-compatible drop-in for request
  var express = require('express');
  var app = express();

  // same verify function as shown above
  function verifyWithJwk(jwt, jwk) {
    var pem;

    if ('EC' === jwk.kty) {
      pem = Eckles.exportSync({ jwk: jwk });
    } else if ('RSA' === jwk.kty) {
      pem = Rasha.exportSync({ jwk: jwk });
    } else {
      throw new Error("Expected EC or RSA key but got '" + jwk.kty + "'");
    }

    return JWT.verify(jwt, pem);
  }

  app.use('/authorized_endpoint', function (req, res, next) {
    // Grab the token from the headers
    var jwt = (req.headers.authorization||'').replace('Bearer ', '');

    // TODO: we'd want to cache keys so we don't fetch them every time
    request({ url: "https://example.com/oidc/jwks/", json: true }, function (resp) {
      var jwk = resp.body.keys[0];

      try {
        verifyWithJwk(jwt, jwk);
      } catch(e) {
        // express will catch this and show the user an error
        throw e;
      }

      // Let the user know the token is invalid
      res.send({ verified: true });
    });
  });

Because they're so small (and more-or-less complete) I vendor them rather than including them as dependencies. So they do get about 20k+ downloads per week, but directly committed into more popular packages like greenlock and rsa-compat that use them.

OIDC & Auth0 Key Fetching

If you're using the Open ID Connect .well-known/openid-configurations jwks_uri or Auth0's .well-known/jwks.json, or another URL type that follows that format, this is the lightweight solution that will allow you to use both RSA and ECDSA JWKs with jsonwebtoken:

  • Keyfetch.js: for fetching pure JWKs (ECDSA & RSA) to verify tokens with jsonwebtoken
var keyfetch = require('keyfetch');
var jwt = require('jsonwebtoken');
var auth = "..."; // some JWT
var token = jwt.decode(auth, { json: true, complete: true })
 
if (!isTrustedIssuer(token.payload.iss)) {
  throw new Error("untrusted issuer");
}
 
keyfetch.oidcJwk(
  token.header.kid
, token.payload.iss
).then(function (result) {
  console.log(result.jwk);
  console.log(result.thumprint);
  console.log(result.pem);
 
  jwt.verify(jwt, pem);
});

jwks-rsa

What I like about the jwks-rsa solution is that it is very specific and constructs an RSA public PEM in about the simplest way possible. In that regard it's even lighter than Rasha.js because it handles even fewer cases (and doing the same for ECDSA would not be that hard).

In fact, I'd probably use it myself if it weren't for the dependency on request - it balloons to 5.9M of dependencies across 48 packages. That just seems insane to me for something that can literally be written in ~400 lines of code (actually, it could be much smaller if you didn't need to maintain API compatibility with request as a drop-in replacement).

jwks-ecdsa

This module is built on outdated code that has dependencies which are no longer necessary (except for special use cases that you probably don't have).

It also uses axios rather than request, which is a lot better in the sense that it has very few dependencies (mostly nearly one-liners that could have reasonably been copied rather than depended on). It's still pretty heavy, but much less of a security risk because there are fewer hands in the pot.

@panva
Copy link
Contributor

panva commented Feb 20, 2019

Hi @solderjs, AJ,

I can see you're the author of rasha and eckles, i've recently looked into using those for a light-on-the-dependencies JOSE replacement for https://github.com/cisco/node-jose dependency for my own projects (oidc client and provider) I am building that's purely for recent node versions with KeyObject support where I don't intend to maintain javascript fallbacks (such as node-jose does with forge).

// Rant alert

Here's my gripe with the current jwk/jose/crypto node ecosystem.

  1. we share this one, everything jwk and jose related is half baked and super heavy on dependencies. This is mostly due to the design of these packages that aim to please everyone, they bundle fallbacks, js implementations for crypto etc. and they lack even the basic EC/RSA key support defined in JWA
  2. until 11.6.0 node required a PEM certificate was passed in, with every crypto operation the certificate was parsed in C land, again and again. Introduction of KeyObject is a massive gain for us using crypto with a finite set of keys. (x2 in ops/sec)

Now on to supporting JWKs with node-jsonwebtoken, I'm all for it for the convenience and for one-time jwk use. BUT, knowing what the cost of js jwk <> asn1 <> pem is, followed up by re-parsing the PEM in openssl this adds up to a lot of time not doing the actual operation and just preparing keys which makes me think i'm better off just using the PEM or in the future an already parsed KeyObject.

ad rasha and eckles,

I took a stab at those packages, very impressive result, much ❤️ for the work you've put in. I wanted to use them for the pure node jose package because of the lack of dependencies they have, but alas

  1. eckles does not support P-521 and we'd have a hard time explaining developers which operations they can do with JWK and which they can't
  2. i'd always favor requiring eckles and rasha rather then bundling them in -> convenience and maintainence effort
  3. there's on-install telemetry bundled with them, i fully understand its purpose and 100% respect it, but it would raise not one eyebrow for a project jsonwebtoken
  4. would you expose a benchmark comparing rasha and eckles with other packages? (namely https://www.npmjs.com/package/@trust/keyto and other similar ones)

The same would apply to jsonwebtoken, @ziluvatar, thoughts?

I'd also separate JWKS and JWK for the sake of this issue, bundling request or similar just for a subset of uses doesn't seem alright. The approach for JWKS would be that the fetching is done OOB and then passed to a keystore like object that can be then passed in to the verify operation. But I digress.

@coolaj86
Copy link

@panva I could add P-521 fairly easily, but is there a significant portion of developers actually using it?Only P-256 and P-384 are actually part of the NIST spec. P-521 is not.

Adding support would merely require adding another header match in the code and I think the byte length is handled differently because it's odd rather than even. Duplicates some code, but not that big of a deal.

I've also just published https://www.npmjs.com/package/keyfetch, which handles fetching from OIDC and Auth0 JWKs URLs and converting to PEM.

I could be talked out of the telemetry if it really made that much difference. npm stats are very limited (and inaccurate). One of the surprising things I learned about some of my projects once I added telemetry was just how many people use node on Windows.

Lastly, why are you using node if performance on conversion operations is so critical? Why not use Go or Rust?

@panva
Copy link
Contributor

panva commented Feb 23, 2019

Lastly, why are you using node if performance on conversion operations is so critical? Why not use Go or Rust?

The choice of language of our customers or even internally in Auth0 varies, but if someone's to use node.js I believe they should be aware they can get way more out of their systems if they just don't convert key formats over and over and use KeyObjects, and the library API design should reflect that to to lead developers to understand the benefits and discourage the use of PEM/JWK inputs for a fairly static key.

node-jws/node-jwa just released with keyobject support, i plan on updating jsonwebtoken with its support as well, plus updating the documentation to point all of the above out

I could be talked out of the telemetry if it really made that much difference. npm stats are very limited (and inaccurate).

+1 on getting rid of it for rasha and eckles, as i wrote to you some weeks ago in an email, i believe you're packing these as a dependency in your other packages so you get much needed telemetry out of the box of those, don't you?

That being said in order for us to pack it with jsonwebtoken and myself with my library (to be published) P-521 would be much appreciated just for completeness sake, as these are server side node libraries a little extra footprint will be far outweighed by the fact there's 0 dependencies, altho maybe they could share some of the internals if they're the same ;) just sayin.

@Slyke
Copy link

Slyke commented Jul 27, 2020

Is this still going to be implemented? I'm currently using https://github.com/Brightspace/node-jwk-to-pem for the meantime.

@coolaj86
Copy link

coolaj86 commented Jul 28, 2020

@root/keypairs is perhaps a more optimal solution, but requires node v10.12+.

@panva: this version does not have telemetry.

Usage

A brief introduction to the APIs:

// generate a new keypair as jwk
// (defaults to EC P-256 when no options are specified)
Keypairs.generate().then(function(pair) {
	console.log(pair.private);
	console.log(pair.public);
});
// JWK to PEM
// (supports various 'format' and 'encoding' options)
return Keypairs.export({ jwk: pair.private, format: 'pkcs8' }).then(function(
	pem
) {
	console.log(pem);
});
// PEM to JWK
return Keypairs.import({ pem: pem }).then(function(jwk) {
	console.log(jwk);
});
// Thumbprint a JWK (SHA256)
return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) {
	console.log(thumb);
});
// Sign a JWT (aka compact JWS)
return Keypairs.signJwt({
  jwk: pair.private
, iss: 'https://example.com'
, exp: '1h'
  // optional claims
, claims: {
  , sub: 'jon.doe@gmail.com'
  }
});

By default ECDSA keys will be used since they've had native support in node
much longer than RSA has, and they're smaller, and faster to generate.

More at https://git.rootprojects.org/root/keypairs.js/src/branch/master/README.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests