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

refactors and new algorithms #978

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2.1
# Thanks to https://github.com/teppeis-sandbox/circleci2-multiple-node-versions

commands:
test-nodejs:
test-modern:
steps:
- run:
name: Versions
Expand All @@ -15,28 +15,50 @@ commands:
- run:
name: Test
command: npm test
test-legacy:
steps:
- run:
name: Versions
command: npm version
- checkout
- run:
name: Install dependencies
command: npm install
- run:
name: Test
command: npm run test-only

jobs:
node-v12:
docker:
- image: node:12
steps:
- test-nodejs
- test-legacy
node-v14:
docker:
- image: node:14
steps:
- test-nodejs
- test-legacy
node-v16:
docker:
- image: node:16
steps:
- test-nodejs
- test-modern
node-v18:
docker:
- image: node:18
steps:
- test-nodejs
- test-modern
node-v20:
docker:
- image: node:20
steps:
- test-modern
node-v22:
docker:
- image: node:22
steps:
- test-modern

workflows:
node-multi-build:
Expand All @@ -45,3 +67,5 @@ workflows:
- node-v14
- node-v16
- node-v18
- node-v20
- node-v22
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
.DS_Store
.nyc_output
coverage
package-lock.json
38 changes: 20 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
# jsonwebtoken

| **Build** | **Dependency** |
|-----------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
| [![Build Status](https://secure.travis-ci.org/auth0/node-jsonwebtoken.svg?branch=master)](http://travis-ci.org/auth0/node-jsonwebtoken) | [![Dependency Status](https://david-dm.org/auth0/node-jsonwebtoken.svg)](https://david-dm.org/auth0/node-jsonwebtoken) |


An implementation of [JSON Web Tokens](https://tools.ietf.org/html/rfc7519).

This was developed against `draft-ietf-oauth-json-web-token-08`. It makes use of [node-jws](https://github.com/brianloveswords/node-jws)

# Install

```bash
Expand All @@ -33,8 +27,8 @@ $ npm install jsonwebtoken

> If `payload` is not a buffer or a string, it will be coerced into a string using `JSON.stringify`.

`secretOrPrivateKey` is a string (utf-8 encoded), buffer, object, or KeyObject containing either the secret for HMAC algorithms or the PEM
encoded private key for RSA and ECDSA. In case of a private key with passphrase an object `{ key, passphrase }` can be used (based on [crypto documentation](https://nodejs.org/api/crypto.html#crypto_sign_sign_private_key_output_format)), in this case be sure you pass the `algorithm` option.
`secretOrPrivateKey` is a [KeyObject] (preferred), string (utf-8 encoded), buffer, object, or any valid input to [crypto.createPrivateKey] or [crypto.createSecretKey] containing either the secret for HMAC algorithms or the private key for RSA, ECDSA, or EdDSA.

When signing with RSA algorithms the minimum modulus length is 2048 except when the allowInsecureKeySizes option is set to true. Private keys below this size will be rejected with an error.

`options`:
Expand All @@ -52,8 +46,8 @@ When signing with RSA algorithms the minimum modulus length is 2048 except when
* `header`
* `keyid`
* `mutatePayload`: if true, the sign function will modify the payload object directly. This is useful if you need a raw reference to the payload after claims have been applied to it but before it has been encoded into a token.
* `allowInsecureKeySizes`: if true allows private keys with a modulus below 2048 to be used for RSA
* `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided.
* `allowInsecureKeySizes`: if true allows private keys with a modulus below 2048 to be used for RSA. This option is intended only for backwards compatibility and should be avoided.
* `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatibility and should be avoided.



Expand Down Expand Up @@ -133,8 +127,8 @@ jwt.sign({

`token` is the JsonWebToken string

`secretOrPublicKey` is a string (utf-8 encoded), buffer, or KeyObject containing either the secret for HMAC algorithms, or the PEM
encoded public key for RSA and ECDSA.
`secretOrPublicKey` is a [KeyObject] (preferred), string (utf-8 encoded), buffer, object, or any valid input to [crypto.createPublicKey] or [crypto.createSecretKey] containing either the secret for HMAC algorithms or the private key for RSA, ECDSA, or EdDSA.

If `jwt.verify` is called asynchronous, `secretOrPublicKey` can be a function that should fetch the secret or public key. See below for a detailed example

As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues/208#issuecomment-231861138), there are other libraries that expect base64 encoded secrets (random bytes encoded using base64), if that is your case you can pass `Buffer.from(secret, 'base64')`, by doing this the secret will be decoded using base64 and the token verification will use the original random bytes.
Expand All @@ -144,9 +138,10 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues
* `algorithms`: List of strings with the names of the allowed algorithms. For instance, `["HS256", "HS384"]`.
> If not specified a defaults will be used based on the type of key provided
> * secret - ['HS256', 'HS384', 'HS512']
> * rsa - ['RS256', 'RS384', 'RS512']
> * ec - ['ES256', 'ES384', 'ES512']
> * default - ['RS256', 'RS384', 'RS512']
> * rsa - ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512']
> * rsa-pss - ['PS256', 'PS384', 'PS512']
> * ec - ['ES256', 'ES256K', 'ES384', 'ES512']
> * ed25519, ed448 - ['EdDSA']
* `audience`: if you want to check audience (`aud`), provide a value here. The audience can be checked against a string, a regular expression or a list of strings and/or regular expressions.
> Eg: `"urn:foo"`, `/urn:f[o]{2}/`, `[/urn:f[o]{2}/, "urn:bar"]`
* `complete`: return an object with the decoded `{ payload, header, signature }` instead of only the usual content of the payload.
Expand All @@ -160,7 +155,7 @@ As mentioned in [this comment](https://github.com/auth0/node-jsonwebtoken/issues
> Eg: `1000`, `"2 days"`, `"10h"`, `"7d"`. A numeric value is interpreted as a seconds count. If you use a string be sure you provide the time units (days, hours, etc), otherwise milliseconds unit is used by default (`"120"` is equal to `"120ms"`).
* `clockTimestamp`: the time in seconds that should be used as the current time for all necessary comparisons.
* `nonce`: if you want to check `nonce` claim, provide a string value here. It is used on Open ID for the ID Tokens. ([Open ID implementation notes](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes))
* `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatability and should be avoided.
* `allowInvalidAsymmetricKeyTypes`: if true, allows asymmetric keys which do not match the specified algorithm. This option is intended only for backwards compatibility and should be avoided.

```js
// verify a token symmetric - synchronous
Expand Down Expand Up @@ -352,9 +347,9 @@ jwt.verify(token, 'shhhhh', function(err, decoded) {
```


## Algorithms supported
## Supported algorithms

Array of supported algorithms. The following algorithms are currently supported.
The following algorithms from the [IANA registry](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms) are supported.

| alg Parameter Value | Digital Signature or MAC Algorithm |
|---------------------|------------------------------------------------------------------------|
Expand All @@ -368,8 +363,10 @@ Array of supported algorithms. The following algorithms are currently supported.
| PS384 | RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0) |
| PS512 | RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0) |
| ES256 | ECDSA using P-256 curve and SHA-256 hash algorithm |
| ES256K | ECDSA using secp256k1 curve and SHA-256 hash algorithm |
| ES384 | ECDSA using P-384 curve and SHA-384 hash algorithm |
| ES512 | ECDSA using P-521 curve and SHA-512 hash algorithm |
| EdDSA | EdDSA using Ed25519 or Ed448 |
| none | No digital signature or MAC value included |

## Refreshing JWTs
Expand All @@ -394,3 +391,8 @@ If you have found a bug or if you have a feature request, please report them at
## License

This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info.

[crypto.createPublicKey]: https://nodejs.org/api/crypto.html#cryptocreatepublickeykey
[crypto.createPrivateKey]: https://nodejs.org/api/crypto.html#cryptocreateprivatekeykey
[crypto.createSecretKey]: https://nodejs.org/api/crypto.html#cryptocreatesecretkeykey-encoding
[KeyObject]: https://nodejs.org/api/crypto.html#class-keyobject
48 changes: 46 additions & 2 deletions decode.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,52 @@
var jws = require('jws');
function payloadFromJWS(encodedPayload, encoding = "utf8") {
try {
return Buffer.from(encodedPayload, "base64").toString(encoding);
} catch (_) {
return;
}
}

function headerFromJWS(encodedHeader) {
try {
return JSON.parse(Buffer.from(encodedHeader, "base64").toString());
} catch (_) {
return;
}
}

const JWS_REGEX = /^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/;

function isValidJws(string) {
return JWS_REGEX.test(string);
}

function jwsDecode(token, opts) {
opts = opts || {};

if (!isValidJws(token)) return null;

let [header, payload, signature] = token.split('.');

header = headerFromJWS(header);
if (header === undefined) return null;

payload = payloadFromJWS(payload);
if (payload === undefined) return null;

if (header.typ === "JWT" || opts.json){
payload = JSON.parse(payload);
}

return {
header,
payload,
signature,
};
}

module.exports = function (jwt, options) {
options = options || {};
var decoded = jws.decode(jwt, options);
const decoded = jwsDecode(jwt, options);
if (!decoded) { return null; }
var payload = decoded.payload;

Expand Down
3 changes: 0 additions & 3 deletions lib/asymmetricKeyDetailsSupported.js

This file was deleted.

9 changes: 9 additions & 0 deletions lib/base64url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* istanbul ignore file */
if (Buffer.isEncoding("base64url")) {
module.exports = (buf) => buf.toString("base64url");
} else {
const fromBase64 = (base64) =>
// eslint-disable-next-line no-div-regex
base64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
module.exports = (buf) => fromBase64(buf.toString("base64"));
}
5 changes: 5 additions & 0 deletions lib/flags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* istanbul ignore file */
const [major, minor] = process.versions.node.split('.').map((v) => parseInt(v, 10));

module.exports.RSA_PSS_KEY_DETAILS_SUPPORTED = major > 16 || (major === 16 && minor >= 9);
module.exports.ASYMMETRIC_KEY_DETAILS_SUPPORTED = major > 15 || (major === 15 && minor >= 7);
63 changes: 63 additions & 0 deletions lib/oneShotAlgs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const { constants } = require("crypto");

module.exports = function(alg, key) {
switch (alg) {
case 'RS256':
return {
digest: 'sha256',
key: { key, padding: constants.RSA_PKCS1_PADDING },
};
case 'RS384':
return {
digest: 'sha384',
key: { key, padding: constants.RSA_PKCS1_PADDING },
};
case 'RS512':
return {
digest: 'sha512',
key: { key, padding: constants.RSA_PKCS1_PADDING },
};
case 'PS256':
return {
digest: 'sha256',
key: { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST },
};
case 'PS384':
return {
digest: 'sha384',
key: { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST },
};
case 'PS512':
return {
digest: 'sha512',
key: { key, padding: constants.RSA_PKCS1_PSS_PADDING, saltLength: constants.RSA_PSS_SALTLEN_DIGEST },
};
case 'ES256':
return {
digest: 'sha256',
key: { key, dsaEncoding: 'ieee-p1363' },
};
case 'ES256K':
return {
digest: 'sha256',
key: { key, dsaEncoding: 'ieee-p1363' },
};
case 'ES384':
return {
digest: 'sha384',
key: { key, dsaEncoding: 'ieee-p1363' },
};
case 'ES512':
return {
digest: 'sha512',
key: { key, dsaEncoding: 'ieee-p1363' },
};
case 'EdDSA':
return {
digest: undefined,
key: { key },
};
default:
throw new Error('unreachable');
}
};
3 changes: 0 additions & 3 deletions lib/psSupported.js

This file was deleted.

3 changes: 0 additions & 3 deletions lib/rsaPssKeyDetailsSupported.js

This file was deleted.

10 changes: 6 additions & 4 deletions lib/validateAsymmetricKey.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
const ASYMMETRIC_KEY_DETAILS_SUPPORTED = require('./asymmetricKeyDetailsSupported');
const RSA_PSS_KEY_DETAILS_SUPPORTED = require('./rsaPssKeyDetailsSupported');
const { ASYMMETRIC_KEY_DETAILS_SUPPORTED, RSA_PSS_KEY_DETAILS_SUPPORTED } = require('./flags');

const allowedAlgorithmsForKeys = {
'ec': ['ES256', 'ES384', 'ES512'],
'ec': ['ES256', 'ES256K', 'ES384', 'ES512'],
'ed25519': ['EdDSA'],
'ed448': ['EdDSA'],
'rsa': ['RS256', 'PS256', 'RS384', 'PS384', 'RS512', 'PS512'],
'rsa-pss': ['PS256', 'PS384', 'PS512']
};

const allowedCurves = {
ES256: 'prime256v1',
ES256K: 'secp256k1',
ES384: 'secp384r1',
ES512: 'secp521r1',
};
Expand Down Expand Up @@ -52,7 +54,7 @@ module.exports = function(algorithm, key) {
const length = parseInt(algorithm.slice(-3), 10);
const { hashAlgorithm, mgf1HashAlgorithm, saltLength } = key.asymmetricKeyDetails;

if (hashAlgorithm !== `sha${length}` || mgf1HashAlgorithm !== hashAlgorithm) {
if (hashAlgorithm !== undefined && (hashAlgorithm !== `sha${length}` || mgf1HashAlgorithm !== hashAlgorithm)) {
throw new Error(`Invalid key for this operation, its RSA-PSS parameters do not meet the requirements of "alg" ${algorithm}.`);
}

Expand Down
9 changes: 5 additions & 4 deletions package.json
Copy link
Member

@ArturKlajnerok ArturKlajnerok Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we bump the version to "version" : "10.0.0" ?

Copy link
Contributor Author

@panva panva Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, this is not breaking the declared supported engines.

Anything blocking us from merging those improvements?

Nothing from my end.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add a few more reviewers?

Seems that ci/circleci is still having some issues
ci/circleci: node-v18 Expected — Waiting for status to be reported

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've linked the actual pipeline results above. In a followup to this @frederikprijck mentioned migrating to github actions.

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"scripts": {
"lint": "eslint .",
"coverage": "nyc mocha --use_strict",
"test": "npm run lint && npm run coverage && cost-of-modules"
"test": "npm run lint && npm run coverage && cost-of-modules",
"test-only": "mocha --use_strict"
},
"repository": {
"type": "git",
Expand All @@ -36,23 +37,23 @@
"url": "https://github.com/auth0/node-jsonwebtoken/issues"
},
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
"ms": "^2.1.1"
},
"devDependencies": {
"atob": "^2.1.2",
"chai": "^4.1.2",
"conventional-changelog": "~1.1.0",
"cost-of-modules": "^1.0.1",
"eslint": "^4.19.1",
"jose": "^5.8.0",
"jws": "^3.2.2",
"mocha": "^5.2.0",
"nsp": "^2.6.2",
"nyc": "^11.9.0",
Expand Down
Loading
Loading