diff --git a/package-lock.json b/package-lock.json index b04a05b96..59f428887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27692,6 +27692,7 @@ "cheerio": "^1.0.0", "crypto-js": "^4.1.1", "json-query": "^2.2.2", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "moment": "^2.29.4", "nanoid": "3.3.8", diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json index d9b4e16e9..47a7a0ffa 100644 --- a/packages/bruno-js/package.json +++ b/packages/bruno-js/package.json @@ -28,6 +28,7 @@ "cheerio": "^1.0.0", "crypto-js": "^4.1.1", "json-query": "^2.2.2", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "moment": "^2.29.4", "nanoid": "3.3.8", diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js index 2a8d02a87..f6d1ed570 100644 --- a/packages/bruno-js/src/runtime/script-runtime.js +++ b/packages/bruno-js/src/runtime/script-runtime.js @@ -30,6 +30,7 @@ const CryptoJS = require('crypto-js'); const NodeVault = require('node-vault'); const xml2js = require('xml2js'); const cheerio = require('cheerio'); +const jsonwebtoken = require('jsonwebtoken'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); class ScriptRuntime { @@ -150,6 +151,7 @@ class ScriptRuntime { 'node-fetch': fetch, 'crypto-js': CryptoJS, 'xml2js': xml2js, + jsonwebtoken, cheerio, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, @@ -284,6 +286,7 @@ class ScriptRuntime { 'node-fetch': fetch, 'crypto-js': CryptoJS, 'xml2js': xml2js, + jsonwebtoken, cheerio, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js index e2d1f4865..df5d81e5a 100644 --- a/packages/bruno-js/src/runtime/test-runtime.js +++ b/packages/bruno-js/src/runtime/test-runtime.js @@ -32,6 +32,7 @@ const CryptoJS = require('crypto-js'); const NodeVault = require('node-vault'); const xml2js = require('xml2js'); const cheerio = require('cheerio'); +const jsonwebtoken = require('jsonwebtoken'); const { executeQuickJsVmAsync } = require('../sandbox/quickjs'); const getResultsSummary = (results) => { @@ -149,7 +150,8 @@ class TestRuntime { res, expect: chai.expect, assert: chai.assert, - __brunoTestResults: __brunoTestResults + __brunoTestResults: __brunoTestResults, + jwt: jsonwebtoken }; if (onConsoleLog && typeof onConsoleLog === 'function') { @@ -209,9 +211,16 @@ class TestRuntime { 'crypto-js': CryptoJS, 'xml2js': xml2js, cheerio, + 'jsonwebtoken': jsonwebtoken, ...whitelistedModules, fs: allowScriptFilesystemAccess ? fs : undefined, 'node-vault': NodeVault + }, + resolve: (moduleName, dirname) => { + console.log('Attempting to resolve module:', moduleName); + console.log('From directory:', dirname); + console.log('Available modules:', Object.keys(vm.require.mock)); + return moduleName; } } }); diff --git a/packages/bruno-js/src/sandbox/bundle-libraries.js b/packages/bruno-js/src/sandbox/bundle-libraries.js index 30d6c7c03..c3ef6549d 100644 --- a/packages/bruno-js/src/sandbox/bundle-libraries.js +++ b/packages/bruno-js/src/sandbox/bundle-libraries.js @@ -12,6 +12,7 @@ const bundleLibraries = async () => { import btoa from "btoa"; import atob from "atob"; import * as CryptoJS from "@usebruno/crypto-js"; + import jwt from "jsonwebtoken"; globalThis.expect = expect; globalThis.assert = assert; globalThis.moment = moment; @@ -19,6 +20,7 @@ const bundleLibraries = async () => { globalThis.atob = atob; globalThis.Buffer = Buffer; globalThis.CryptoJS = CryptoJS; + globalThis.jwt = jwt; globalThis.requireObject = { ...(globalThis.requireObject || {}), 'chai': { expect, assert }, @@ -26,7 +28,8 @@ const bundleLibraries = async () => { 'buffer': { Buffer }, 'btoa': btoa, 'atob': atob, - 'crypto-js': CryptoJS + 'crypto-js': CryptoJS, + 'jsonwebtoken': jwt }; `; diff --git a/packages/bruno-tests/collection/scripting/inbuilt modules/jwt/jwt.bru b/packages/bruno-tests/collection/scripting/inbuilt modules/jwt/jwt.bru new file mode 100644 index 000000000..70774f53e --- /dev/null +++ b/packages/bruno-tests/collection/scripting/inbuilt modules/jwt/jwt.bru @@ -0,0 +1,110 @@ +meta { + name: jwt + type: http + seq: 1 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +script:pre-request { + const jwt = require('jsonwebtoken'); + + // Create a payload + const payload = { + userId: "12345", + username: "testuser", + role: "admin", + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + (60 * 60) // expires in 1 hour + }; + + // Sign the token + const secret = "test-secret-key"; + const token = jwt.sign(payload, secret); + + // Store the token and payload for later verification + bru.setVar("jwt-token", token); + bru.setVar("jwt-payload", payload); + bru.setVar("jwt-secret", secret); + + // Set the token in the Authorization header + req.setHeader("Authorization", `Bearer ${token}`); +} + +script:post-response { + const jwt = require('jsonwebtoken'); + const token = bru.getVar("jwt-token"); + const secret = bru.getVar("jwt-secret"); + + // Decode the token + const decoded = jwt.decode(token); + bru.setVar("jwt-decoded", decoded); + + // Verify the token + try { + const verified = jwt.verify(token, secret); + bru.setVar("jwt-verified", verified); + } catch (error) { + bru.setVar("jwt-verification-error", error.message); + } +} + +tests { + const jwt = require('jsonwebtoken'); + + test("JWT token should be properly generated", function() { + const token = bru.getVar("jwt-token"); + expect(token).to.be.a('string'); + expect(token.split('.')).to.have.lengthOf(3); // JWT should have 3 parts + }); + + test("Decoded token should match original payload", function() { + const decoded = bru.getVar("jwt-decoded"); + const payload = bru.getVar("jwt-payload"); + + expect(decoded.userId).to.equal(payload.userId); + expect(decoded.username).to.equal(payload.username); + expect(decoded.role).to.equal(payload.role); + }); + + test("Token should be successfully verified", function() { + const verified = bru.getVar("jwt-verified"); + const payload = bru.getVar("jwt-payload"); + + expect(verified.userId).to.equal(payload.userId); + expect(verified.username).to.equal(payload.username); + expect(verified.role).to.equal(payload.role); + }); + + test("Token should have valid expiration", function() { + const decoded = bru.getVar("jwt-decoded"); + const now = Math.floor(Date.now() / 1000); + + expect(decoded.exp).to.be.a('number'); + expect(decoded.exp).to.be.greaterThan(now); + }); + + test("Token should have valid issued at time", function() { + const decoded = bru.getVar("jwt-decoded"); + const now = Math.floor(Date.now() / 1000); + + expect(decoded.iat).to.be.a('number'); + expect(decoded.iat).to.be.lessThanOrEqual(now); + }); + + test("Invalid token verification should fail", async function() { + const token = bru.getVar("jwt-token"); + const invalidSecret = "wrong-secret-key"; + + try { + jwt.verify(token, invalidSecret); + expect.fail("Token verification should have failed with wrong secret"); + } catch (error) { + expect(error.message).to.include("invalid signature"); + } + }); +}