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], + } + ], } ]);