diff --git a/.github/workflows/awaiting-response-label.yml b/.github/workflows/awaiting-response-label.yml
new file mode 100644
index 00000000..3feba0a6
--- /dev/null
+++ b/.github/workflows/awaiting-response-label.yml
@@ -0,0 +1,82 @@
+name: Awaiting Response Label Sync
+
+on:
+ # Fires when someone finishes reviewing a PR (e.g. picks "Request changes")
+ pull_request_review:
+ types: [submitted]
+ # Fires when the PR author pushes new commits
+ pull_request_target:
+ types: [synchronize]
+ # Fires when someone comments on a PR (also fires for plain issues, filtered out below)
+ issue_comment:
+ types: [created]
+
+permissions:
+ pull-requests: write
+ issues: write
+ contents: read
+
+jobs:
+ sync-label:
+ # issue_comment fires for issues too, so only run it for PR comments
+ if: >-
+ github.event_name != 'issue_comment' ||
+ github.event.issue.pull_request != null
+ runs-on: ubuntu-latest
+ steps:
+ - name: Sync "awaiting response" label
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 #v9.0.0
+ env:
+ AWAITING_LABEL: 'awaiting response'
+ with:
+ script: |
+ const awaitingLabel = process.env.AWAITING_LABEL;
+ // Resolve the PR number for whichever event triggered this run
+ const prNumber = context.eventName === 'issue_comment'
+ ? context.payload.issue.number
+ : context.payload.pull_request.number;
+ const { owner, repo } = context.repo;
+
+ // Check whether the label is already on the PR, so we don't add it twice or
+ // try to remove something that isn't there
+ const { data: issue } = await github.rest.issues.get({
+ owner, repo, issue_number: prNumber,
+ });
+ const hasLabel = issue.labels.some(l =>
+ (typeof l === 'string' ? l : l.name) === awaitingLabel
+ );
+
+ // A reviewer chose "Request changes" -> the author needs to respond
+ if (context.eventName === 'pull_request_review' && context.payload.review.state === 'changes_requested') {
+ if (!hasLabel) {
+ await github.rest.issues.addLabels({
+ owner, repo, issue_number: prNumber, labels: [awaitingLabel],
+ });
+ core.info(`Added "${awaitingLabel}".`);
+ } else {
+ core.info('Label already applied.');
+ }
+ // The author pushed new commits -> treat that as their response and clear the label
+ } else if (context.eventName === 'pull_request_target' && context.payload.action === 'synchronize') {
+ if (hasLabel) {
+ // If the label was already gone for some reason, that's fine, not an error
+ await github.rest.issues.removeLabel({
+ owner, repo, issue_number: prNumber, name: awaitingLabel,
+ }).catch(e => core.warning(`removeLabel failed: ${e.message}`));
+ core.info(`Removed "${awaitingLabel}".`);
+ } else {
+ core.info('Label not present; nothing to do.');
+ }
+ // The PR author left a comment -> treat any reply from them as a response
+ } else if (context.eventName === 'issue_comment' && context.payload.comment.user.login === context.payload.issue.user.login) {
+ if (hasLabel) {
+ await github.rest.issues.removeLabel({
+ owner, repo, issue_number: prNumber, name: awaitingLabel,
+ }).catch(e => core.warning(`removeLabel failed: ${e.message}`));
+ core.info(`Removed "${awaitingLabel}".`);
+ } else {
+ core.info('Label not present; nothing to do.');
+ }
+ } else {
+ core.info('Event does not require a label change.');
+ }
diff --git a/package-lock.json b/package-lock.json
index eabf953c..2967baf0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -53,6 +53,7 @@
"highlight.js": "^11.11.1",
"ieee754": "^1.2.1",
"jimp": "1.6.0",
+ "jose": "^6.2.3",
"jq-web": "^0.5.1",
"jquery": "3.7.1",
"js-ascon": "^1.3.0",
@@ -62,7 +63,6 @@
"json5": "^2.2.3",
"jsonata": "^2.2.2",
"jsonpath-plus": "^10.4.0",
- "jsonwebtoken": "9.0.3",
"jsqr": "^1.4.0",
"jsrsasign": "^11.1.3",
"kbpgp": "^2.1.17",
@@ -6486,12 +6486,6 @@
"node": "*"
}
},
- "node_modules/buffer-equal-constant-time": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
- "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
- "license": "BSD-3-Clause"
- },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -8833,15 +8827,6 @@
"stream-shift": "^1.0.0"
}
},
- "node_modules/ecdsa-sig-formatter": {
- "version": "1.0.11",
- "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
- "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "safe-buffer": "^5.0.1"
- }
- },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -12496,6 +12481,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jose": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
+ "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
@@ -12696,40 +12690,6 @@
"node": ">=18.0.0"
}
},
- "node_modules/jsonwebtoken": {
- "version": "9.0.3",
- "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
- "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
- "license": "MIT",
- "dependencies": {
- "jws": "^4.0.1",
- "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"
- },
- "engines": {
- "node": ">=12",
- "npm": ">=6"
- }
- },
- "node_modules/jsonwebtoken/node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/jsqr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
@@ -12755,27 +12715,6 @@
"setimmediate": "^1.0.5"
}
},
- "node_modules/jwa": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
- "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
- "license": "MIT",
- "dependencies": {
- "buffer-equal-constant-time": "^1.0.1",
- "ecdsa-sig-formatter": "1.0.11",
- "safe-buffer": "^5.0.1"
- }
- },
- "node_modules/jws": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
- "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
- "license": "MIT",
- "dependencies": {
- "jwa": "^2.0.1",
- "safe-buffer": "^5.0.1"
- }
- },
"node_modules/kbpgp": {
"version": "2.1.17",
"resolved": "https://registry.npmjs.org/kbpgp/-/kbpgp-2.1.17.tgz",
@@ -13046,18 +12985,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/lodash.includes": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
- "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
- "license": "MIT"
- },
- "node_modules/lodash.isboolean": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
- "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
- "license": "MIT"
- },
"node_modules/lodash.isfinite": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz",
@@ -13065,28 +12992,11 @@
"dev": true,
"license": "MIT"
},
- "node_modules/lodash.isinteger": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
- "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
- "license": "MIT"
- },
- "node_modules/lodash.isnumber": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
- "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
- "license": "MIT"
- },
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
- "license": "MIT"
- },
- "node_modules/lodash.isstring": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
- "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/lodash.merge": {
@@ -13096,12 +13006,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/lodash.once": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
- "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
- "license": "MIT"
- },
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
@@ -13828,6 +13732,7 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/multicast-dns": {
diff --git a/package.json b/package.json
index 8cd81c9e..a7fe47dc 100644
--- a/package.json
+++ b/package.json
@@ -138,6 +138,7 @@
"highlight.js": "^11.11.1",
"ieee754": "^1.2.1",
"jimp": "1.6.0",
+ "jose": "^6.2.3",
"jq-web": "^0.5.1",
"jquery": "3.7.1",
"js-ascon": "^1.3.0",
@@ -147,7 +148,6 @@
"json5": "^2.2.3",
"jsonata": "^2.2.2",
"jsonpath-plus": "^10.4.0",
- "jsonwebtoken": "9.0.3",
"jsqr": "^1.4.0",
"jsrsasign": "^11.1.3",
"kbpgp": "^2.1.17",
diff --git a/src/core/lib/RSA.mjs b/src/core/lib/RSA.mjs
index 9037379c..ca1c2e80 100644
--- a/src/core/lib/RSA.mjs
+++ b/src/core/lib/RSA.mjs
@@ -7,6 +7,7 @@
*/
import forge from "node-forge";
+import * as asn1js from "asn1js";
export const MD_ALGORITHMS = {
"SHA-1": forge.md.sha1,
@@ -15,3 +16,48 @@ export const MD_ALGORITHMS = {
"SHA-384": forge.md.sha384,
"SHA-512": forge.md.sha512,
};
+
+const rsaEncryptionOID = "1.2.840.113549.1.1.1";
+
+/**
+ * Convert PKCS#1 RSA public key (PEM) to SPKI PEM
+ * @param {string} originalPem
+ * @returns {string}
+ */
+export function pkcs1ToSpki(originalPem) {
+ // remove PEM headers
+ const b64 = originalPem
+ .replace(/-----BEGIN RSA PUBLIC KEY-----/g, "")
+ .replace(/-----END RSA PUBLIC KEY-----/g, "")
+ .replace(/\s+/g, "");
+
+ const pkcs1Der = Uint8Array.from(atob(b64), c => c.charCodeAt(0)).buffer;
+
+ // SubjectPublicKeyInfo ::= SEQUENCE {
+ // algorithm AlgorithmIdentifier,
+ // subjectPublicKey BIT STRING
+ // }
+
+ const spkiSchema = new asn1js.Sequence({
+ value: [
+ new asn1js.Sequence({
+ value: [
+ // rsaEncryption OID
+ new asn1js.ObjectIdentifier({ value: rsaEncryptionOID }),
+ new asn1js.Null()
+ ]
+ }),
+ new asn1js.BitString({ valueHex: pkcs1Der })
+ ]
+ });
+
+ const spkiDer = spkiSchema.toBER(false);
+
+ const spkiB64 = btoa(
+ String.fromCharCode(...new Uint8Array(spkiDer))
+ );
+
+ const lines = spkiB64.match(/.{1,64}/g).join("\n");
+
+ return `-----BEGIN PUBLIC KEY-----\n${lines}\n-----END PUBLIC KEY-----`;
+}
diff --git a/src/core/operations/JWTDecode.mjs b/src/core/operations/JWTDecode.mjs
index b6356b5a..0072d7c1 100644
--- a/src/core/operations/JWTDecode.mjs
+++ b/src/core/operations/JWTDecode.mjs
@@ -5,7 +5,7 @@
*/
import Operation from "../Operation.mjs";
-import jwt from "jsonwebtoken";
+import { decodeJwt } from "jose";
import OperationError from "../errors/OperationError.mjs";
/**
@@ -42,14 +42,11 @@ class JWTDecode extends Operation {
*/
run(input, args) {
try {
- const decoded = jwt.decode(input, {
- json: true,
- complete: true
- });
-
- return decoded.payload;
+ return decodeJwt(input);
} catch (err) {
- throw new OperationError(err);
+ throw new OperationError(`Invalid JWT: could not decode token.
+
+${err}`);
}
}
diff --git a/src/core/operations/JWTVerify.mjs b/src/core/operations/JWTVerify.mjs
index 451b82ab..08b3b7e5 100644
--- a/src/core/operations/JWTVerify.mjs
+++ b/src/core/operations/JWTVerify.mjs
@@ -4,9 +4,10 @@
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
-import jwt from "jsonwebtoken";
+import { jwtVerify, importSPKI, importX509, decodeProtectedHeader } from "jose";
import OperationError from "../errors/OperationError.mjs";
import {JWT_ALGORITHMS} from "../lib/JWT.mjs";
+import {pkcs1ToSpki} from "../lib/RSA.mjs";
/**
@@ -22,7 +23,7 @@ class JWTVerify extends Operation {
this.name = "JWT Verify";
this.module = "Crypto";
- this.description = "Verifies that a JSON Web Token is valid and has been signed with the provided secret / private key.
The key should be either the secret for HMAC algorithms or the PEM-encoded public key for RSA and ECDSA.";
+ this.description = "Verifies that a JSON Web Token is valid and has been signed with the provided secret / public key.
The key should be either the secret for HMAC algorithms or the PEM-encoded public key (or certificate) for RSA and ECDSA.
Expiry (exp) and not-before (nbf) claims are validated if present. Unsigned tokens (alg: none) are rejected; use 'JWT Decode' to view their payload.";
this.infoURL = "https://wikipedia.org/wiki/JSON_Web_Token";
this.inputType = "string";
this.outputType = "JSON";
@@ -38,23 +39,64 @@ class JWTVerify extends Operation {
/**
* @param {string} input
* @param {Object[]} args
- * @returns {string}
+ * @returns {JSON}
*/
- run(input, args) {
+ async run(input, args) {
const [key] = args;
- const algos = JWT_ALGORITHMS;
- algos[algos.indexOf("None")] = "none";
+ const algorithms = JWT_ALGORITHMS.filter(a => a !== "None");
+
+ let header;
+ try {
+ header = decodeProtectedHeader(input);
+ } catch (err) {
+ throw new OperationError(`Invalid JWT format.
+
+${err}`);
+ }
+
+ if (!header.alg || header.alg === "none") {
+ throw new OperationError("This token is unsigned (\"alg\": \"none\") and cannot be verified. Use the 'JWT Decode' operation to view its payload.");
+ }
+
+ if (!algorithms.includes(header.alg)) {
+ throw new OperationError(`The token's algorithm "${header.alg}" is not supported. Supported algorithms are: ${algorithms.join(", ")}.`);
+ }
+
+ let secret;
+ try {
+ if (key.startsWith("-----BEGIN PUBLIC KEY-----")) {
+ secret = await importSPKI(key, header.alg);
+ } else if (key.startsWith("-----BEGIN RSA PUBLIC KEY-----")) {
+ secret = await importSPKI(pkcs1ToSpki(key), header.alg);
+ } else if (key.startsWith("-----BEGIN CERTIFICATE-----")) {
+ secret = await importX509(key, header.alg);
+ } else {
+ secret = new TextEncoder().encode(key);
+ }
+ } catch (err) {
+ throw new OperationError(`Error: Have you entered the key correctly? The key should be either the secret for HMAC algorithms or the PEM-encoded public key for RSA and ECDSA.
+
+${err}`);
+ }
try {
- const verified = jwt.verify(input, key, { algorithms: algos });
-
- if (Object.prototype.hasOwnProperty.call(verified, "name") && verified.name === "JsonWebTokenError") {
- throw new OperationError(verified.message);
- }
-
- return verified;
+ const { payload } = await jwtVerify(input, secret, { algorithms });
+ return payload;
} catch (err) {
- throw new OperationError(err);
+ switch (err.code) {
+ case "ERR_JWT_EXPIRED":
+ throw new OperationError(`The token has expired.
+
+${err.message}`);
+ case "ERR_JWS_SIGNATURE_VERIFICATION_FAILED":
+ throw new OperationError("Invalid signature. Have you entered the correct key?");
+ case "ERR_JWT_CLAIM_VALIDATION_FAILED":
+ throw new OperationError(`Token claim validation failed.
+
+${err.message}`);
+ default:
+ throw new OperationError(err.toString());
+ }
}
}
diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js
index 867dc4d5..6f2699b7 100644
--- a/tests/browser/02_ops.js
+++ b/tests/browser/02_ops.js
@@ -219,9 +219,9 @@ module.exports = {
// testOp(browser, "JSON Minify", "test input", "test_output");
// testOp(browser, "JSON to CSV", "test input", "test_output");
testOp(browser, "Jsonata Query", '{"a": "SGVsbG8gV29ybGQh"}', '"Hello World!"', ["$base64decode($.a)"]);
- // testOp(browser, "JWT Decode", "test input", "test_output");
+ testOp(browser, "JWT Decode", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.0ha6-j4FwvEIKPVZ-hf3S_R9Hy_UtXzq4dnedXcUrXk", '{\n "String": "SomeString",\n "Number": 42,\n "iat": 1\n}');
// testOp(browser, "JWT Sign", "test input", "test_output");
- // testOp(browser, "JWT Verify", "test input", "test_output");
+ testOp(browser, "JWT Verify", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.0ha6-j4FwvEIKPVZ-hf3S_R9Hy_UtXzq4dnedXcUrXk", '{\n "String": "SomeString",\n "Number": 42,\n "iat": 1\n}', ["secret_cat"]);
// testOp(browser, "JavaScript Beautify", "test input", "test_output");
// testOp(browser, "JavaScript Minify", "test input", "test_output");
// testOp(browser, "JavaScript Parser", "test input", "test_output");
diff --git a/tests/operations/tests/JWTDecode.mjs b/tests/operations/tests/JWTDecode.mjs
index 1ef47f81..c2002169 100644
--- a/tests/operations/tests/JWTDecode.mjs
+++ b/tests/operations/tests/JWTDecode.mjs
@@ -47,5 +47,18 @@ TestRegister.addTests([
args: [],
}
],
+ },
+ {
+ name: "JWT Decode: malformed token",
+ input: "not a jwt",
+ expectedOutput: `Invalid JWT: could not decode token.
+
+JWTInvalid: Invalid JWT`,
+ recipeConfig: [
+ {
+ op: "JWT Decode",
+ args: [],
+ }
+ ],
}
]);
diff --git a/tests/operations/tests/JWTVerify.mjs b/tests/operations/tests/JWTVerify.mjs
index 0a0817cc..cd854797 100644
--- a/tests/operations/tests/JWTVerify.mjs
+++ b/tests/operations/tests/JWTVerify.mjs
@@ -16,23 +16,55 @@ const outputObject = JSON.stringify({
const hsKey = "secret_cat";
/* Retaining private key as a comment
-const rsPriv = `-----BEGIN RSA PRIVATE KEY-----
-MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw
-33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW
-+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB
-AoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS
-3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5Cp
-uGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE
-2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0
-GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0K
-Su5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY
-6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5
-fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523
-Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aP
-FaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==
------END RSA PRIVATE KEY-----`;
+const rsPriv = `-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDlhnQq4PFKzMOV
+qM1FyJkLv2oadstOD+3jxzokG2n48HdE7rSnBR1bbu4peSjvMK4Pg++qwwi/Q2V5
+J/o97utd92P7+Qa/pz7JXZnNU6xKo7snYF3oKZXt63B75ia7/Gz7P5fC+KoBNlIu
+4dFTdb0Mp/Ld8QvXuOrqMZBUbsYpxW/4FY0hWhimCXsItHogX41X7/AaXVLL/5fO
+3o2yi78S3kbimqBAk6BQHFUQwl2Ktf432pWG7o6PvcMZHu5XKvphUYNItOc7KGuY
+Qf83AYWfwttWO7pVw9wIQLsNQOg4dF2exNWOguZiJl5ri0E1sHOwm/3uZWGZc41G
+qVNz6BfXAgMBAAECggEABTXLy0a3PiXEcXrsdfS7bKXYCGkWl7C1i2HJXb/K5F4T
+sa/iz+vGIn4zz83dR64I4CH0PcxsQBM36XZ72ri4BQu0QahXJotAWfJ9pvM1n610
+dndcilmZ+qrz2bbRaEZZnvZu69EYjYB91cHwg37SX3LXM4Hg7UZYUXVLO8TBS3oR
+2spOUdC+jWZlK/irnu9SVqCYxPJ4GlTCVEtw81arbhVtVmSzYxoEJJWPRHLEOPK8
+vwPDjzOzuQeDtHWuEriDsXe1iiVZA+06QfiI3YL60kdZqCNQeAr4AyXEZ4KHDuPZ
+XKO8WE7zIJR/KOdFmQnMSjyVSgI9ZSNROr/n8FphYQKBgQD9k9gb5gzUL6UhRjpg
+JnQxENGfZX2IE2ftqIlRqgquiv+haEmSuK9uFOksBPmuHL12xD6E29HWZ5eRZinw
+j1MTSPn9YSBMSId3ZCx7fMWulI9GkD14ERCrwPTasqj0q2+qbLavf6DEkvAYgGs1
+j5hBexw3Cs0MsAupK8uel9GapwKBgQDnt8liEU2MXiM0lZdA4JNv52UWVawLSTjZ
+SnPD9Uvfs7e9qFAsQLHoBKTRKxjRNOduUCFjC0LDKK6V+D1Ykhw0iO2/v7v79F13
+WlJm+11Wrw0LxRSVF8TJ7WSeV3NQoeneiInDY/SkVAJKuananAuxQa5vbVdjL9Yw
+f2VNkPKvUQKBgDQPeWvo5DNgAMf/EhcRbgNwz4ipHoekbE7nriGdBoplMSxjK8+6
+qRGxq1mFP/mWHvPWQHuoYcFiDUUls3CXDFhGQETb/vSKY32IjHVh3XgFOhEjpesT
+ndeVSu/nhDaPR+K2ZR9M1aXmAPMW0mvxqjGvY9CNICH9trcpralGxBq7AoGBALtl
+BedaiPWxGBWpuOYN+Q6x+y2h4X5LHD+Wl6LtmQv0Iu+t1BDFzVsPjl9e1DY068lp
+4mZgMAD2wKKmXK5pk4flCSlenV+5E3G3yRZhyO2Wqh1PqEKfM1X+t7XakXavSxze
+HVO4XZwcGLJoDnauZkJXFQQmUFr1z+nuzXV6eNnRAoGAYIpPHO5B6UtCPdK8p3jk
+WYA790gGN/t6vMRw7qtFGBKt3DmlcTp7GHZxbF5p6yGKFPIo/mHGebdibTF4qamP
+Rkqrshupx/m+4udn6kDNyi1mHJeWjoLod6qA8BaJIh2NqEcim6FQp6MfYVC84m91
+KedDUWDtk6YFQKGxIDOrz/k=
+-----END PRIVATE KEY-----`;
*/
const rsPub = `-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5YZ0KuDxSszDlajNRciZ
+C79qGnbLTg/t48c6JBtp+PB3RO60pwUdW27uKXko7zCuD4PvqsMIv0NleSf6Pe7r
+Xfdj+/kGv6c+yV2ZzVOsSqO7J2Bd6CmV7etwe+Ymu/xs+z+XwviqATZSLuHRU3W9
+DKfy3fEL17jq6jGQVG7GKcVv+BWNIVoYpgl7CLR6IF+NV+/wGl1Sy/+Xzt6Nsou/
+Et5G4pqgQJOgUBxVEMJdirX+N9qVhu6Oj73DGR7uVyr6YVGDSLTnOyhrmEH/NwGF
+n8LbVju6VcPcCEC7DUDoOHRdnsTVjoLmYiZea4tBNbBzsJv97mVhmXONRqlTc+gX
+1wIDAQAB
+-----END PUBLIC KEY-----`;
+// Same public key as rsPub, in PKCS#1 format
+const rsPubPkcs1 = `-----BEGIN RSA PUBLIC KEY-----
+MIIBCgKCAQEA5YZ0KuDxSszDlajNRciZC79qGnbLTg/t48c6JBtp+PB3RO60pwUd
+W27uKXko7zCuD4PvqsMIv0NleSf6Pe7rXfdj+/kGv6c+yV2ZzVOsSqO7J2Bd6CmV
+7etwe+Ymu/xs+z+XwviqATZSLuHRU3W9DKfy3fEL17jq6jGQVG7GKcVv+BWNIVoY
+pgl7CLR6IF+NV+/wGl1Sy/+Xzt6Nsou/Et5G4pqgQJOgUBxVEMJdirX+N9qVhu6O
+j73DGR7uVyr6YVGDSLTnOyhrmEH/NwGFn8LbVju6VcPcCEC7DUDoOHRdnsTVjoLm
+YiZea4tBNbBzsJv97mVhmXONRqlTc+gX1wIDAQAB
+-----END RSA PUBLIC KEY-----`;
+// 1024-bit key: jose refuses RSA keys below 2048 bits
+const rsPubWeak = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd
UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs
HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D
@@ -50,10 +82,20 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----`;
+const hsToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.0ha6-j4FwvEIKPVZ-hf3S_R9Hy_UtXzq4dnedXcUrXk";
+const rsToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.GbJLourP2hTpBMm0mPrDOKSjsOnVU_teH0h7CCW333uT-56axQLR8pKcLyTBYt8wx5cZVk4lunqA95GCmz8upGN9U_Hzc6aCBaoXzbb9i0TGp-7kLOUP_UN0_5f96SWDHF4hb2P38Q1ONXMbI7hTbkphc4RlDpqSjNeFvzMgJpNjAVeIpzLoyhDqJbx_D6-iGQCXdHRgEpF8xNjZHHVADv00G0riIQvRsvJ3Q4XUiQDbtXCHfOWMIUWobLEFfI9I4se0RMRditm5UytOyrAuUl4QhskU3bofD8p7UiM9BjBHmTMepCJM__D1qrWjv5ugcYV7yVgerBKAoYF2LSAS2A";
+// Same claims as rsToken, signed with the 1024-bit key pair
+const rsTokenWeak = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.MjEJhtZk2nXzigi24piMzANmrj3mILHJcDl0xOjl5a8EgdKVL1oaMEjTkMQp5RA8YrqeRBFaX-BGGCKOXn5zPY1DJwWsBUyN9C-wGR2Qye0eogH_3b4M9EW00TPCUPXm2rx8URFj7Wg9VlsmrGzLV2oKkPgkVxuFSxnpO3yjn1Y";
+const esToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.WkECT51jSfpRkcpQ4x0h5Dwe7CFBI6u6Et2gWp91HC7mpN_qCFadRpsvJLtKubm6cJTLa68xtei0YrDD8fxIUA";
+// Same claims as hsToken plus "exp": 1, signed with hsKey
+const hsTokenExpired = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxLCJleHAiOjF9.ZKrXhOiSg4rsEY77HurSJY1i4-cpFYkr6m_TTA2ChFM";
+// Unsigned token: header {"alg":"none"}
+const noneToken = "eyJhbGciOiJub25lIn0.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.";
+
TestRegister.addTests([
{
name: "JWT Verify: HS",
- input: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.0ha6-j4FwvEIKPVZ-hf3S_R9Hy_UtXzq4dnedXcUrXk",
+ input: hsToken,
expectedOutput: outputObject,
recipeConfig: [
{
@@ -64,7 +106,7 @@ TestRegister.addTests([
},
{
name: "JWT Verify: RS",
- input: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.MjEJhtZk2nXzigi24piMzANmrj3mILHJcDl0xOjl5a8EgdKVL1oaMEjTkMQp5RA8YrqeRBFaX-BGGCKOXn5zPY1DJwWsBUyN9C-wGR2Qye0eogH_3b4M9EW00TPCUPXm2rx8URFj7Wg9VlsmrGzLV2oKkPgkVxuFSxnpO3yjn1Y",
+ input: rsToken,
expectedOutput: outputObject,
recipeConfig: [
{
@@ -73,9 +115,31 @@ TestRegister.addTests([
}
],
},
+ {
+ name: "JWT Verify: RS with PKCS#1 public key",
+ input: rsToken,
+ expectedOutput: outputObject,
+ recipeConfig: [
+ {
+ op: "JWT Verify",
+ args: [rsPubPkcs1],
+ }
+ ],
+ },
+ {
+ name: "JWT Verify: RS with key shorter than 2048 bits",
+ input: rsTokenWeak,
+ expectedOutput: "TypeError: RS256 requires key modulusLength to be 2048 bits or larger",
+ recipeConfig: [
+ {
+ op: "JWT Verify",
+ args: [rsPubWeak],
+ }
+ ],
+ },
{
name: "JWT Verify: ES",
- input: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJTdHJpbmciOiJTb21lU3RyaW5nIiwiTnVtYmVyIjo0MiwiaWF0IjoxfQ.WkECT51jSfpRkcpQ4x0h5Dwe7CFBI6u6Et2gWp91HC7mpN_qCFadRpsvJLtKubm6cJTLa68xtei0YrDD8fxIUA",
+ input: esToken,
expectedOutput: outputObject,
recipeConfig: [
{
@@ -83,5 +147,40 @@ TestRegister.addTests([
args: [esPub],
}
],
+ },
+ {
+ name: "JWT Verify: wrong key",
+ input: hsToken,
+ expectedOutput: "Invalid signature. Have you entered the correct key?",
+ recipeConfig: [
+ {
+ op: "JWT Verify",
+ args: ["wrong_secret"],
+ }
+ ],
+ },
+ {
+ name: "JWT Verify: expired token",
+ input: hsTokenExpired,
+ expectedOutput: `The token has expired.
+
+"exp" claim timestamp check failed`,
+ recipeConfig: [
+ {
+ op: "JWT Verify",
+ args: [hsKey],
+ }
+ ],
+ },
+ {
+ name: "JWT Verify: unsigned token",
+ input: noneToken,
+ expectedOutput: "This token is unsigned (\"alg\": \"none\") and cannot be verified. Use the 'JWT Decode' operation to view its payload.",
+ recipeConfig: [
+ {
+ op: "JWT Verify",
+ args: [hsKey],
+ }
+ ],
}
]);