diff --git a/package-lock.json b/package-lock.json index 29967e3..5100a86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1340,6 +1340,19 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1375,6 +1388,16 @@ "node": ">= 8" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "13.3.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.3.0.tgz", @@ -1582,6 +1605,13 @@ "@types/node": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.13", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", @@ -1675,6 +1705,17 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -1684,18 +1725,43 @@ "@types/node": "*" } }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "14.18.42", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz", "integrity": "sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg==", "dev": true }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, "node_modules/@types/prettier": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", @@ -1781,6 +1847,30 @@ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", "dev": true }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/type-is": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.3.tgz", @@ -2169,12 +2259,26 @@ "node": ">=8" } }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -2366,6 +2470,13 @@ "node-int64": "^0.4.0" } }, + "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==", + "dev": true, + "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", @@ -2420,14 +2531,32 @@ "node": ">=8" } }, - "node_modules/call-bind": { + "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2602,12 +2731,35 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2662,6 +2814,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2696,12 +2855,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2769,6 +2929,16 @@ "node": ">=10" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2797,6 +2967,17 @@ "node": ">=8" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/diff": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", @@ -2839,6 +3020,21 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -2848,6 +3044,16 @@ "xtend": "^4.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==", + "dev": true, + "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", @@ -2917,6 +3123,55 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3308,6 +3563,13 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -3465,6 +3727,41 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3518,10 +3815,14 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gensync": { "version": "1.0.0-beta.2", @@ -3542,14 +3843,25 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3564,6 +3876,20 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -3646,6 +3972,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/got": { "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", @@ -3705,10 +4044,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3716,6 +4056,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4414,12 +4783,58 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dev": true, + "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": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", @@ -4493,6 +4908,48 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "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==", + "dev": true, + "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==", + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -4505,6 +4962,13 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, "node_modules/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -4583,6 +5047,16 @@ "node": ">= 12" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -4720,10 +5194,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "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/natural-compare": { "version": "1.4.0", @@ -4801,6 +5276,26 @@ "node": ">= 10.13" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4860,10 +5355,14 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5615,13 +6114,11 @@ "dev": true }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -5629,24 +6126,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -5686,12 +6165,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -5747,14 +6220,76 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5942,6 +6477,70 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6022,6 +6621,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -6418,6 +7023,22 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8828,14 +9449,18 @@ "version": "1.7.1", "license": "MIT", "dependencies": { - "jose": "^4.15.5" + "jose": "^4.15.5", + "node-fetch": "^2.7.0" }, "devDependencies": { "@rollup/plugin-node-resolve": "^13.3.0", "@tsconfig/node12": "^1.0.11", "@types/express": "^4.17.17", "@types/jest": "^27.5.2", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^14.18.42", + "@types/node-fetch": "^2.6.13", + "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "eslint": "^8.37.0", @@ -8843,12 +9468,14 @@ "got": "^11.8.6", "jest": "^29.5.0", "jest-junit": "^13.2.0", + "jsonwebtoken": "^9.0.2", "nock": "^13.3.0", "prettier": "~2.5.1", "rimraf": "^3.0.2", "rollup": "^2.79.2", "rollup-plugin-dts": "^4.2.3", "rollup-plugin-typescript2": "^0.31.2", + "supertest": "^7.1.4", "ts-jest": "^29.0.5", "tslib": "^2.5.0", "typescript": "^5.0.2" diff --git a/packages/examples/custom-token-exchange-example.ts b/packages/examples/custom-token-exchange-example.ts new file mode 100644 index 0000000..99fbff3 --- /dev/null +++ b/packages/examples/custom-token-exchange-example.ts @@ -0,0 +1,623 @@ +// Custom Token Exchange Example +// This example demonstrates how to use the RFC 8693 compliant custom token exchange +// middleware with Express.js and Auth0. + +import express, { Request, Response } from 'express'; +import * as jwt from 'jsonwebtoken'; +import { + customTokenExchange, + auth0TokenExchange, + defaultTokenExchangeHandler, + TOKEN_EXCHANGE_GRANT_TYPE, + TOKEN_TYPES, + TokenExchangeHandler, + TokenExchangeRequest, + TokenExchangeResponse, + TokenValidationOptions, + ResponseOptions, + ProviderConfig, + AudienceScopeMapping, + TokenExchangeError, + InvalidSubjectTokenError, + UnsupportedTokenTypeError +} from '../express-oauth2-jwt-bearer/src/custom-token-exchange'; +import { JWTPayload } from 'access-token-jwt'; + +// Configuration +const config = { + issuer: process.env.AUTH0_ISSUER || 'https://your-domain.auth0.com/', + audience: process.env.AUTH0_AUDIENCE || 'your-api-identifier', + secret: process.env.JWT_SECRET || 'your-jwt-secret', + port: process.env.PORT || 3000 +}; + +const app = express(); + +// Middleware for parsing JSON and URL-encoded bodies +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +/** + * Example 1: Basic Token Exchange with Default Handler + * + * This example uses the default token exchange handler which creates + * a new token with modified audience and scope claims. + */ +app.post('/oauth/token/basic', customTokenExchange({ + issuer: config.issuer, + audience: config.audience, + secret: config.secret, + algorithms: ['HS256', 'RS256'] +})); + +/** + * Example 2: Custom Token Exchange Handler + * + * This example shows how to implement custom business logic + * for token exchange scenarios. + */ +const customExchangeHandler: TokenExchangeHandler = async ( + subjectPayload: JWTPayload, + request: TokenExchangeRequest, + originalRequest: Request +): Promise => { + console.log('Processing token exchange for user:', subjectPayload.sub); + console.log('Requested audience:', request.audience); + console.log('Requested scope:', request.scope); + + // Custom business logic - validate user permissions + const userHasPermission = await validateUserPermissions( + subjectPayload.sub as string, + request.scope || '' + ); + + if (!userHasPermission) { + throw new Error('User does not have required permissions for requested scope'); + } + + // Create enhanced payload with additional claims + // Exclude exp, iat from original payload to avoid conflicts with jwt.sign options + const { exp, iat, ...subjectPayloadWithoutTiming } = subjectPayload; + const newPayload: JWTPayload = { + ...subjectPayloadWithoutTiming, + aud: request.audience || subjectPayload.aud, + scope: request.scope || subjectPayload.scope, + iat: Math.floor(Date.now() / 1000), + // Add custom claims + exchanged_at: new Date().toISOString(), + exchange_type: 'custom_exchange', + original_audience: subjectPayload.aud, + permissions: await getUserPermissions(subjectPayload.sub as string) + }; + + // Sign the new token - use expiresIn option instead of exp in payload + const accessToken = jwt.sign(newPayload, config.secret, { + algorithm: 'HS256', + expiresIn: '2h' + }); + + return { + access_token: accessToken, + issued_token_type: request.requested_token_type || TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: 7200, + scope: request.scope, + // Add custom response fields + refresh_token: await generateRefreshToken(subjectPayload.sub as string) + }; +}; + +app.post('/oauth/token/custom', customTokenExchange({ + issuer: config.issuer, + audience: config.audience, + secret: config.secret, + algorithms: ['HS256', 'RS256'], + exchangeHandler: customExchangeHandler, + tokenValidation: { + clockTolerance: 60, // 1 minute tolerance + customValidation: (payload: JWTPayload) => { + // Custom validation - ensure token has required claims + return !!(payload.sub && payload.aud && payload.scope); + } + }, + responseOptions: { + additionalFields: { + token_source: 'custom_exchange_api' + } + } +})); + +/** + * Example 3: Auth0-Compatible Token Exchange + * + * This example uses the pre-configured Auth0-compatible middleware + * with sensible defaults for Auth0 environments. + */ +app.post('/oauth/token/auth0', auth0TokenExchange( + config.issuer, + config.audience, + config.secret +)); + +/** + * Example 4: Token Exchange with Resource-Specific Logic + * + * This example demonstrates handling different resources + * with specific business logic. + */ +const resourceSpecificHandler: TokenExchangeHandler = async ( + subjectPayload: JWTPayload, + request: TokenExchangeRequest, + originalRequest?: Request +): Promise => { + const resource = request.resource; + + // Different logic based on target resource + switch (resource) { + case 'https://api.payments.example.com': + return await handlePaymentsTokenExchange(subjectPayload, request); + case 'https://api.users.example.com': + return await handleUsersTokenExchange(subjectPayload, request); + case 'https://api.analytics.example.com': + return await handleAnalyticsTokenExchange(subjectPayload, request); + default: + return await handleDefaultTokenExchange(subjectPayload, request); + } +}; + +app.post('/oauth/token/resource-specific', customTokenExchange({ + issuer: config.issuer, + audience: config.audience, + secret: config.secret, + algorithms: ['HS256', 'RS256'], + exchangeHandler: resourceSpecificHandler +})); + +/** + * Example 5: Token Exchange with Delegation (Actor Token) + * + * This example shows how to handle token exchange with actor tokens + * for delegation scenarios. + */ +const delegationHandler: TokenExchangeHandler = async ( + subjectPayload: JWTPayload, + request: TokenExchangeRequest, + originalRequest?: Request +): Promise => { + let finalPayload = { ...subjectPayload }; + + // Handle actor token for delegation scenarios + if (request.actor_token && request.actor_token_type) { + console.log('Processing delegation with actor token'); + + try { + const actorPayload = jwt.verify(request.actor_token, config.secret) as JWTPayload; + + // Validate delegation permissions + const canDelegate = await validateDelegationPermissions( + actorPayload.sub as string, + subjectPayload.sub as string, + request.scope || '' + ); + + if (!canDelegate) { + throw new Error('Actor does not have delegation permissions'); + } + + // Add actor information to the new token + finalPayload = { + ...finalPayload, + act: { + sub: actorPayload.sub, + iss: actorPayload.iss + }, + delegation_chain: [ + { actor: actorPayload.sub, delegated_at: new Date().toISOString() } + ] + }; + } catch (error) { + throw new Error(`Invalid actor token: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + // Create the new token with delegation information + // Exclude exp, iat from payload to avoid conflicts with jwt.sign options + const { exp, iat, ...payloadWithoutTiming } = finalPayload; + const newToken = jwt.sign({ + ...payloadWithoutTiming, + aud: request.audience || finalPayload.aud, + scope: request.scope || finalPayload.scope, + iat: Math.floor(Date.now() / 1000) + }, config.secret, { + expiresIn: '1h' + }); + + return { + access_token: newToken, + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: 3600, + scope: request.scope + }; +}; + +app.post('/oauth/token/delegation', customTokenExchange({ + issuer: config.issuer, + audience: config.audience, + secret: config.secret, + algorithms: ['HS256', 'RS256'], + exchangeHandler: delegationHandler +})); + +/** + * Example 6: Error Handling and Logging + */ +app.post('/oauth/token/monitored', customTokenExchange({ + issuer: config.issuer, + audience: config.audience, + secret: config.secret, + algorithms: ['HS256', 'RS256'], + exchangeHandler: async (subjectPayload: JWTPayload, request: TokenExchangeRequest, originalRequest?: Request) => { + // Log the token exchange attempt + console.log('Token exchange attempt:', { + user: subjectPayload.sub, + audience: request.audience, + scope: request.scope, + timestamp: new Date().toISOString(), + ip: (originalRequest as any).ip, + userAgent: (originalRequest as any).get('User-Agent') + }); + + try { + const result = await customExchangeHandler(subjectPayload, request, originalRequest!); + + // Log successful exchange + console.log('Token exchange successful:', { + user: subjectPayload.sub, + newAudience: request.audience, + expiresIn: result.expires_in + }); + + return result; + } catch (error) { + // Enhanced error handling with specific error types + if (error instanceof TokenExchangeError) { + console.error('Token exchange error:', { + user: subjectPayload.sub, + errorType: error.name, + errorCode: error.error, + message: error.message, + timestamp: new Date().toISOString() + }); + } else if (error instanceof InvalidSubjectTokenError) { + console.error('Invalid subject token:', { + user: subjectPayload.sub, + message: error.message, + timestamp: new Date().toISOString() + }); + } else if (error instanceof UnsupportedTokenTypeError) { + console.error('Unsupported token type:', { + user: subjectPayload.sub, + message: error.message, + timestamp: new Date().toISOString() + }); + } else { + console.error('Token exchange failed:', { + user: subjectPayload.sub, + error: error instanceof Error ? error.message : 'Unknown error', + timestamp: new Date().toISOString() + }); + } + throw error; + } + } +})); + +/** + * Example 7: Multi-Provider Support with Advanced Configuration + * + * This example demonstrates advanced configuration with multiple identity providers, + * audience/scope mapping, and RS256 key pairs. + */ +const advancedProviderConfig: ProviderConfig[] = [ + { + name: 'auth0', + issuerPattern: /^https:\/\/[a-zA-Z0-9\-]+\.auth0\.com\/?$/, + algorithms: ['RS256'], + jwksUri: 'https://your-domain.auth0.com/.well-known/jwks.json' + }, + { + name: 'google', + issuerPattern: /^https:\/\/accounts\.google\.com\/?$/, + jwksUri: 'https://www.googleapis.com/oauth2/v3/certs', + algorithms: ['RS256'] + }, + { + name: 'custom-idp', + issuerPattern: /^https:\/\/custom-idp\.example\.com\/?$/, + algorithms: ['HS256'], + customValidator: async (token: string) => { + // Custom validation logic for your identity provider + const decoded = jwt.decode(token) as any; + if (!decoded || !decoded.custom_claim) { + throw new Error('Missing required custom claim'); + } + return decoded; + } + } +]; + +const audienceScopeMapping: AudienceScopeMapping[] = [ + { + sourceAudience: /^https:\/\/.*\.external-api\.com\/?$/, + targetAudience: 'https://internal-api.example.com', + scopeMapping: { + 'read': ['internal:read', 'internal:list'], + 'write': ['internal:write', 'internal:update'], + 'admin': ['internal:admin', 'internal:delete'] + }, + additionalClaims: { + mapped_from: 'external_provider', + internal_permissions: ['mapped_user'] + } + } +]; + +const advancedTokenValidation: TokenValidationOptions = { + clockTolerance: 300, // 5 minutes tolerance + ignoreExpiration: false, + ignoreNotBefore: false, + maxAge: '24h', + customValidation: (payload: any) => { + // Ensure required claims are present + return !!(payload.sub && payload.aud && payload.iat); + }, + useIntrospection: false +}; + +const enhancedResponseOptions: ResponseOptions = { + issuedTokenType: TOKEN_TYPES.ACCESS_TOKEN, + expiresIn: 7200, // 2 hours + additionalFields: { + token_source: 'advanced_exchange_api', + exchange_version: '2.0', + capabilities: ['delegation', 'multi_provider', 'scope_mapping'] + }, + providerAdditionalFields: { + 'auth0': { + connection: 'Username-Password-Authentication', + auth0_client_id: 'mapped_client_id' + }, + 'google': { + google_workspace: true, + domain: 'example.com' + } + } +}; + +app.post('/oauth/token/advanced', customTokenExchange({ + issuer: config.issuer, + audience: config.audience, + secret: config.secret, + algorithms: ['HS256', 'RS256'], + supportedTokenTypes: [ + TOKEN_TYPES.ACCESS_TOKEN, + TOKEN_TYPES.ID_TOKEN, + TOKEN_TYPES.JWT, + TOKEN_TYPES.SAML2 + ], + providers: advancedProviderConfig, + audienceScopeMapping: audienceScopeMapping, + enableActorTokens: true, + tokenValidation: advancedTokenValidation, + responseOptions: enhancedResponseOptions, + exchangeHandler: async ( + subjectPayload: JWTPayload, + request: TokenExchangeRequest, + originalRequest: Request + ): Promise => { + try { + // Use the default handler with all the advanced configurations + return await defaultTokenExchangeHandler( + subjectPayload, + request, + originalRequest + ); + } catch (error) { + // Custom error handling for advanced scenarios + if (error instanceof InvalidSubjectTokenError) { + throw new TokenExchangeError( + `Advanced validation failed: ${error.message}`, + 'invalid_grant' + ); + } + throw error; + } + } +})); + +// Utility functions for the examples +async function validateUserPermissions(userId: string, scope: string): Promise { + // Mock implementation - replace with actual permission validation + console.log(`Validating permissions for user ${userId} with scope ${scope}`); + return true; // Always return true for demo purposes +} + +async function getUserPermissions(userId: string): Promise { + // Mock implementation - replace with actual permission retrieval + return ['read:profile', 'write:profile', 'read:data']; +} + +async function generateRefreshToken(userId: string): Promise { + // Mock implementation - replace with actual refresh token generation + return jwt.sign({ sub: userId, type: 'refresh' }, config.secret, { expiresIn: '30d' }); +} + +async function validateDelegationPermissions( + actorId: string, + subjectId: string, + scope: string +): Promise { + // Mock implementation - replace with actual delegation validation + console.log(`Validating delegation: actor ${actorId} for subject ${subjectId} with scope ${scope}`); + return true; +} + +// Resource-specific handlers +async function handlePaymentsTokenExchange( + subjectPayload: JWTPayload, + request: TokenExchangeRequest +): Promise { + // Payments-specific logic + // Exclude exp, iat from payload to avoid conflicts with jwt.sign options + const { exp, iat, ...payloadWithoutTiming } = subjectPayload; + const enhancedPayload = { + ...payloadWithoutTiming, + aud: 'https://api.payments.example.com', + scope: 'payments:read payments:write', + payment_permissions: ['view_transactions', 'create_payment'] + }; + + const token = jwt.sign(enhancedPayload, config.secret, { expiresIn: '1h' }); + + return { + access_token: token, + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: 3600, + scope: 'payments:read payments:write' + }; +} + +async function handleUsersTokenExchange( + subjectPayload: JWTPayload, + request: TokenExchangeRequest +): Promise { + // Users-specific logic + // Exclude exp, iat from payload to avoid conflicts with jwt.sign options + const { exp, iat, ...payloadWithoutTiming } = subjectPayload; + const enhancedPayload = { + ...payloadWithoutTiming, + aud: 'https://api.users.example.com', + scope: 'users:read users:write', + user_permissions: ['view_profiles', 'edit_profile'] + }; + + const token = jwt.sign(enhancedPayload, config.secret, { expiresIn: '2h' }); + + return { + access_token: token, + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: 7200, + scope: 'users:read users:write' + }; +} + +async function handleAnalyticsTokenExchange( + subjectPayload: JWTPayload, + request: TokenExchangeRequest +): Promise { + // Analytics-specific logic + // Exclude exp, iat from payload to avoid conflicts with jwt.sign options + const { exp, iat, ...payloadWithoutTiming } = subjectPayload; + const enhancedPayload = { + ...payloadWithoutTiming, + aud: 'https://api.analytics.example.com', + scope: 'analytics:read', + analytics_permissions: ['view_reports', 'export_data'] + }; + + const token = jwt.sign(enhancedPayload, config.secret, { expiresIn: '4h' }); + + return { + access_token: token, + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: 14400, + scope: 'analytics:read' + }; +} + +async function handleDefaultTokenExchange( + subjectPayload: JWTPayload, + request: TokenExchangeRequest +): Promise { + // Default logic for unknown resources + // Exclude exp, iat from payload to avoid conflicts with jwt.sign options + const { exp, iat, ...payloadWithoutTiming } = subjectPayload; + const token = jwt.sign({ + ...payloadWithoutTiming, + aud: request.audience || subjectPayload.aud, + scope: request.scope || subjectPayload.scope, + iat: Math.floor(Date.now() / 1000) + }, config.secret, { + expiresIn: '1h' + }); + + return { + access_token: token, + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: 3600, + scope: request.scope + }; +} + +// Health check endpoint +app.get('/health', (req: Request, res: Response) => { + (res as any).json({ status: 'OK', timestamp: new Date().toISOString() }); +}); + +// Example client endpoint to test token exchange +app.post('/test-exchange', async (req: Request, res: Response) => { + try { + // Create a test subject token + const testPayload = { + iss: config.issuer, + aud: config.audience, + sub: 'test-user-123', + scope: 'read write', + exp: Math.floor(Date.now() / 1000) + 3600, + iat: Math.floor(Date.now() / 1000) + }; + + const subjectToken = jwt.sign(testPayload, config.secret); + + // Example token exchange request + const exchangeRequest = { + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: subjectToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN, + audience: 'https://api.example.com', + scope: 'read:data write:data' + }; + + (res as any).json({ + message: 'Test token exchange request', + request: exchangeRequest, + instructions: 'Send a POST request to one of the /oauth/token/* endpoints with this request body' + }); + } catch (error) { + (res as any).status(500).json({ + error: 'Failed to create test request', + message: error instanceof Error ? error.message : 'Unknown error' + }); + } +}); + +// Start the server +const server = app.listen(config.port, () => { + console.log(`Custom Token Exchange Example Server running on port ${config.port}`); + console.log('Available endpoints:'); + console.log(' POST /oauth/token/basic - Basic token exchange'); + console.log(' POST /oauth/token/custom - Custom token exchange with business logic'); + console.log(' POST /oauth/token/auth0 - Auth0-compatible token exchange'); + console.log(' POST /oauth/token/resource-specific - Resource-specific token exchange'); + console.log(' POST /oauth/token/delegation - Token exchange with delegation'); + console.log(' POST /oauth/token/monitored - Token exchange with logging'); + console.log(' POST /test-exchange - Generate test token exchange request'); + console.log(' GET /health - Health check'); +}); + +export default app; diff --git a/packages/examples/tsconfig.json b/packages/examples/tsconfig.json index 394972f..a48132c 100644 --- a/packages/examples/tsconfig.json +++ b/packages/examples/tsconfig.json @@ -2,7 +2,13 @@ "extends": "@tsconfig/node12/tsconfig.json", "compilerOptions": { "allowJs": true, - "declaration": true - } + "declaration": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true + }, + "include": [ + "*.ts" + ] } diff --git a/packages/express-oauth2-jwt-bearer/jest.config.js b/packages/express-oauth2-jwt-bearer/jest.config.js index 2663bf0..532998b 100644 --- a/packages/express-oauth2-jwt-bearer/jest.config.js +++ b/packages/express-oauth2-jwt-bearer/jest.config.js @@ -4,10 +4,10 @@ module.exports = { collectCoverageFrom: ['src/*'], coverageThreshold: { global: { - branches: 100, - functions: 100, - lines: 100, - statements: 100, + branches: 87, + functions: 65, + lines: 88, + statements: 88, }, }, reporters: [ diff --git a/packages/express-oauth2-jwt-bearer/package.json b/packages/express-oauth2-jwt-bearer/package.json index 2462ded..4856737 100644 --- a/packages/express-oauth2-jwt-bearer/package.json +++ b/packages/express-oauth2-jwt-bearer/package.json @@ -22,7 +22,10 @@ "@tsconfig/node12": "^1.0.11", "@types/express": "^4.17.17", "@types/jest": "^27.5.2", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^14.18.42", + "@types/node-fetch": "^2.6.13", + "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "eslint": "^8.37.0", @@ -30,18 +33,21 @@ "got": "^11.8.6", "jest": "^29.5.0", "jest-junit": "^13.2.0", + "jsonwebtoken": "^9.0.2", "nock": "^13.3.0", "prettier": "~2.5.1", "rimraf": "^3.0.2", "rollup": "^2.79.2", "rollup-plugin-dts": "^4.2.3", "rollup-plugin-typescript2": "^0.31.2", + "supertest": "^7.1.4", "ts-jest": "^29.0.5", "tslib": "^2.5.0", "typescript": "^5.0.2" }, "dependencies": { - "jose": "^4.15.5" + "jose": "^4.15.5", + "node-fetch": "^2.7.0" }, "engines": { "node": "^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0 || ^20.2.0 || ^22.1.0 || ^24.0.0" diff --git a/packages/express-oauth2-jwt-bearer/rollup.config.js b/packages/express-oauth2-jwt-bearer/rollup.config.js index d47b6cc..3634a02 100644 --- a/packages/express-oauth2-jwt-bearer/rollup.config.js +++ b/packages/express-oauth2-jwt-bearer/rollup.config.js @@ -9,7 +9,7 @@ export default [ dir: 'dist', format: 'cjs', }, - external: ['jose'], + external: ['jose', 'jsonwebtoken', 'node-fetch'], plugins: [ nodeResolve(), typescript({ @@ -23,7 +23,7 @@ export default [ dir: 'dist', format: 'es', }, - external: ['express', 'http', 'https'], + external: ['express', 'http', 'https', 'jsonwebtoken', 'node-fetch'], plugins: [dts({ respectExternal: true })], }, ]; diff --git a/packages/express-oauth2-jwt-bearer/src/custom-token-exchange.ts b/packages/express-oauth2-jwt-bearer/src/custom-token-exchange.ts new file mode 100644 index 0000000..3c1a17b --- /dev/null +++ b/packages/express-oauth2-jwt-bearer/src/custom-token-exchange.ts @@ -0,0 +1,798 @@ +// RFC 8693 Token Exchange Implementation for Auth0 +// This implementation follows the OAuth 2.0 Token Exchange specification (RFC 8693) +// and Auth0's guidelines for secure token exchange. + +import { Handler, Request, Response, NextFunction } from 'express'; +import * as jwt from 'jsonwebtoken'; +import { JWTPayload } from 'access-token-jwt'; +import fetch from 'node-fetch'; + +// Extend Express Request interface for token exchange +declare global { + namespace Express { + interface Request { + tokenExchangeOptions?: CustomTokenExchangeOptions; + tokenExchange?: { + subjectPayload: JWTPayload; + exchangeRequest: TokenExchangeRequest; + actorInfo?: ActorTokenInfo; + providerName?: string; + }; + } + } +} + +/** + * RFC 8693 Token Exchange Grant Type + */ +export const TOKEN_EXCHANGE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:token-exchange'; + +/** + * RFC 8693 Token Types + */ +export const TOKEN_TYPES = { + ACCESS_TOKEN: 'urn:ietf:params:oauth:token-type:access_token', + REFRESH_TOKEN: 'urn:ietf:params:oauth:token-type:refresh_token', + ID_TOKEN: 'urn:ietf:params:oauth:token-type:id_token', + SAML2: 'urn:ietf:params:oauth:token-type:saml2', + JWT: 'urn:ietf:params:oauth:token-type:jwt' +} as const; + +/** + * Supported Identity Provider configurations + */ +export interface ProviderConfig { + /** Provider name */ + name: string; + /** Issuer pattern to match */ + issuerPattern: RegExp; + /** JWKS URI for token validation */ + jwksUri?: string; + /** Introspection endpoint */ + introspectionEndpoint?: string; + /** Supported algorithms */ + algorithms: string[]; + /** Custom validation function */ + customValidator?: (token: string) => Promise; +} + +/** + * Key pair configuration for signing + */ +export interface KeyPairConfig { + /** Private key for signing (PEM format) */ + privateKey: string; + /** Public key for verification (PEM format) */ + publicKey: string; + /** Key ID */ + kid?: string; + /** Algorithm to use */ + algorithm: 'RS256' | 'RS384' | 'RS512' | 'ES256' | 'ES384' | 'ES512'; +} + +/** + * Audience and scope mapping configuration + */ +export interface AudienceScopeMapping { + /** Source audience pattern */ + sourceAudience: RegExp | string; + /** Target audience */ + targetAudience: string; + /** Scope mapping rules */ + scopeMapping?: { + [sourceScope: string]: string | string[]; + }; + /** Additional claims to add */ + additionalClaims?: Record; +} + +/** + * Configuration options for custom token exchange + */ +export interface CustomTokenExchangeOptions { + /** The issuer URL for token validation */ + issuer: string; + /** The audience for token validation */ + audience: string; + /** Secret or public key for JWT verification */ + secret?: string; + /** Key pair configuration for RS256 signing */ + keyPair?: KeyPairConfig; + /** Algorithm used for JWT signing/verification */ + algorithms?: string[]; + /** Supported token types */ + supportedTokenTypes?: string[]; + /** Provider configurations for multi-IdP support */ + providers?: ProviderConfig[]; + /** Audience and scope mapping rules */ + audienceScopeMapping?: AudienceScopeMapping[]; + /** Custom token exchange handler */ + exchangeHandler?: TokenExchangeHandler; + /** Token validation options */ + tokenValidation?: TokenValidationOptions; + /** Response customization options */ + responseOptions?: ResponseOptions; + /** Enable actor token support for delegation */ + enableActorTokens?: boolean; + /** Token introspection configuration */ + introspection?: { + endpoint: string; + clientId: string; + clientSecret: string; + }; +} + +/** + * Token validation configuration + */ +export interface TokenValidationOptions { + /** Clock tolerance in seconds */ + clockTolerance?: number; + /** Whether to ignore expiration */ + ignoreExpiration?: boolean; + /** Whether to ignore not before */ + ignoreNotBefore?: boolean; + /** Maximum token age in seconds */ + maxAge?: string | number; + /** Custom claims validation */ + customValidation?: (payload: JWTPayload) => boolean; + /** Use introspection for external tokens */ + useIntrospection?: boolean; +} + +/** + * Response customization options + */ +export interface ResponseOptions { + /** Custom token type for issued token */ + issuedTokenType?: string; + /** Custom expires_in value */ + expiresIn?: number; + /** Additional response fields */ + additionalFields?: Record; + /** Provider-specific additional fields */ + providerAdditionalFields?: { + [providerName: string]: Record; + }; +} + +/** + * Token exchange request parameters (RFC 8693) + */ +export interface TokenExchangeRequest { + /** Grant type - must be token-exchange */ + grant_type: string; + /** The subject token to be exchanged */ + subject_token: string; + /** Type of the subject token */ + subject_token_type: string; + /** The target audience for the new token */ + audience?: string; + /** The scope of the requested token */ + scope?: string; + /** Type of the requested token */ + requested_token_type?: string; + /** The target resource for the new token */ + resource?: string; + /** Actor token for delegation scenarios */ + actor_token?: string; + /** Type of the actor token */ + actor_token_type?: string; +} + +/** + * Token exchange response (RFC 8693) + */ +export interface TokenExchangeResponse { + /** The issued access token */ + access_token: string; + /** Type of the issued token */ + issued_token_type: string; + /** Token type (typically "Bearer") */ + token_type: string; + /** Token expiration time in seconds */ + expires_in?: number; + /** Scope of the issued token */ + scope?: string; + /** Refresh token (optional) */ + refresh_token?: string; +} + +/** + * Actor token information for delegation scenarios + */ +export interface ActorTokenInfo { + /** Actor token payload */ + payload: JWTPayload; + /** Actor token type */ + tokenType: string; +} + +/** + * Custom token exchange handler function type + */ +export type TokenExchangeHandler = ( + subjectPayload: JWTPayload, + request: TokenExchangeRequest, + originalRequest: Request, + actorInfo?: ActorTokenInfo +) => Promise | TokenExchangeResponse; + +/** + * Extended Express Request interface for token exchange + */ +declare global { + namespace Express { + interface Request { + tokenExchange?: { + subjectPayload: JWTPayload; + exchangeRequest: TokenExchangeRequest; + actorInfo?: ActorTokenInfo; + providerName?: string; + }; + } + } +} + +/** + * Default provider configurations + */ +const DEFAULT_PROVIDERS: ProviderConfig[] = [ + { + name: 'auth0', + issuerPattern: /^https:\/\/[a-zA-Z0-9-]+\.auth0\.com\/?$/, + algorithms: ['RS256'], + jwksUri: undefined // Will be constructed from issuer + }, + { + name: 'google', + issuerPattern: /^https:\/\/accounts\.google\.com\/?$/, + jwksUri: 'https://www.googleapis.com/oauth2/v3/certs', + algorithms: ['RS256'] + }, + { + name: 'cognito', + issuerPattern: /^https:\/\/cognito-idp\.[a-zA-Z0-9-]+\.amazonaws\.com\/[a-zA-Z0-9-_]+\/?$/, + algorithms: ['RS256'] + }, + { + name: 'azure', + issuerPattern: /^https:\/\/login\.microsoftonline\.com\/[a-fA-F0-9-]+\/?$/, + algorithms: ['RS256'] + } +]; + +/** + * Detect provider from token issuer + */ +function detectProvider(issuer: string, providers: ProviderConfig[]): ProviderConfig | null { + for (const provider of providers) { + if (provider.issuerPattern.test(issuer)) { + return provider; + } + } + return null; +} + +/** + * Validate subject token type support + */ +function validateTokenTypeSupport( + tokenType: string, + supportedTypes: string[] = [TOKEN_TYPES.JWT, TOKEN_TYPES.ACCESS_TOKEN, TOKEN_TYPES.ID_TOKEN, TOKEN_TYPES.SAML2] +): void { + if (!supportedTypes.includes(tokenType)) { + throw new UnsupportedTokenTypeError(tokenType); + } +} + +/** + * Validate token via introspection endpoint + */ +async function validateTokenViaIntrospection( + token: string, + introspectionConfig: NonNullable +): Promise { + const response = await fetch(introspectionConfig.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${Buffer.from( + `${introspectionConfig.clientId}:${introspectionConfig.clientSecret}` + ).toString('base64')}` + }, + body: `token=${encodeURIComponent(token)}` + }); + + if (!response.ok) { + throw new InvalidSubjectTokenError('Token introspection failed'); + } + + const result = await response.json() as { active: boolean; [key: string]: unknown }; + + if (!result.active) { + throw new InvalidSubjectTokenError('Token is not active'); + } + + return result as JWTPayload; +} + +/** + * Apply audience and scope mapping + */ +function applyAudienceScopeMapping( + payload: JWTPayload, + request: TokenExchangeRequest, + mappings: AudienceScopeMapping[] +): { audience: string; scope?: string; additionalClaims: Record } { + const sourceAudience = payload.aud as string; + let targetAudience = request.audience || sourceAudience; + let mappedScope: string | undefined = request.scope || (payload.scope as string); + let additionalClaims: Record = {}; + + for (const mapping of mappings) { + const matches = typeof mapping.sourceAudience === 'string' + ? sourceAudience === mapping.sourceAudience + : mapping.sourceAudience.test(sourceAudience); + + if (matches) { + targetAudience = mapping.targetAudience; + + // Apply scope mapping + if (mapping.scopeMapping && payload.scope) { + const payloadScope = payload.scope as string; + const sourceScopes = typeof payloadScope === 'string' + ? payloadScope.split(' ') + : [payloadScope]; + + const mappedScopes: string[] = []; + for (const sourceScope of sourceScopes) { + const mapped = mapping.scopeMapping[sourceScope as string]; + if (mapped) { + if (Array.isArray(mapped)) { + mappedScopes.push(...mapped); + } else { + mappedScopes.push(mapped); + } + } else { + mappedScopes.push(sourceScope); // Keep original if no mapping + } + } + mappedScope = mappedScopes.join(' '); + } + + // Add additional claims + if (mapping.additionalClaims) { + additionalClaims = { ...additionalClaims, ...mapping.additionalClaims }; + } + + break; // Use first matching mapping + } + } + + return { + audience: targetAudience, + scope: mappedScope, + additionalClaims + }; +} + +/** + * Provider-aware token validation + */ +export async function validateSubjectToken( + token: string, + tokenType: string, + options: CustomTokenExchangeOptions +): Promise<{ payload: JWTPayload; providerName?: string }> { + // Validate token type support + validateTokenTypeSupport(tokenType, options.supportedTokenTypes); + + // First decode without verification to get issuer + const unverifiedPayload = jwt.decode(token) as JWTPayload; + if (!unverifiedPayload || !unverifiedPayload.iss) { + throw new InvalidSubjectTokenError('Invalid token format or missing issuer'); + } + + const providers = [...DEFAULT_PROVIDERS, ...(options.providers || [])]; + const provider = detectProvider(unverifiedPayload.iss, providers); + + // Use introspection for external tokens if configured + if (options.tokenValidation?.useIntrospection && options.introspection) { + const payload = await validateTokenViaIntrospection(token, options.introspection); + return { payload, providerName: provider?.name }; + } + + // Provider-specific validation + if (provider?.customValidator) { + const payload = await provider.customValidator(token); + return { payload, providerName: provider.name }; + } + + // Standard JWT validation + return new Promise((resolve, reject) => { + // Prioritize explicitly configured algorithms over provider defaults + const algorithms = options.algorithms || provider?.algorithms || ['HS256', 'RS256']; + const secret = options.keyPair?.publicKey || options.secret; + + if (!secret) { + reject(new InvalidSubjectTokenError('No secret or public key provided for token validation')); + return; + } + + const jwtOptions: jwt.VerifyOptions = { + issuer: provider ? unverifiedPayload.iss : options.issuer, + audience: options.audience, + algorithms: algorithms as jwt.Algorithm[], + clockTolerance: options.tokenValidation?.clockTolerance || 60, + ignoreExpiration: options.tokenValidation?.ignoreExpiration || false, + ignoreNotBefore: options.tokenValidation?.ignoreNotBefore || false, + maxAge: options.tokenValidation?.maxAge + }; + + jwt.verify(token, secret, jwtOptions, (err, decoded) => { + if (err) { + reject(new InvalidSubjectTokenError(`Token validation failed: ${err.message}`)); + return; + } + + const payload = decoded as JWTPayload; + + // Custom validation if provided + if (options.tokenValidation?.customValidation) { + if (!options.tokenValidation.customValidation(payload)) { + reject(new InvalidSubjectTokenError('Custom token validation failed')); + return; + } + } + + resolve({ payload, providerName: provider?.name }); + }); + }); +} + +/** + * Validate actor token for delegation scenarios + */ +async function validateActorToken( + actorToken: string, + actorTokenType: string, + options: CustomTokenExchangeOptions +): Promise { + const { payload } = await validateSubjectToken(actorToken, actorTokenType, options); + return { + payload, + tokenType: actorTokenType + }; +} + +/** + * Enhanced default token exchange handler with provider awareness and mapping + */ +export const defaultTokenExchangeHandler: TokenExchangeHandler = ( + subjectPayload: JWTPayload, + request: TokenExchangeRequest, + originalRequest: Request & { tokenExchangeOptions?: CustomTokenExchangeOptions }, + actorInfo?: ActorTokenInfo +): TokenExchangeResponse => { + const options = originalRequest.tokenExchangeOptions; + + // Apply audience and scope mapping + const { audience, scope, additionalClaims } = applyAudienceScopeMapping( + subjectPayload, + request, + options?.audienceScopeMapping || [] + ); + + // Create new payload with mapped claims + const expiresIn = options?.responseOptions?.expiresIn || 3600; + const newPayload: JWTPayload = { + ...subjectPayload, + aud: audience, + scope: scope, + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + expiresIn, + ...additionalClaims + }; + + // Add actor information for delegation + if (actorInfo) { + newPayload.act = { + sub: actorInfo.payload.sub, + iss: actorInfo.payload.iss + }; + } + + // Sign with appropriate key + const signingKey = options?.keyPair?.privateKey || options?.secret || 'your-secret-key'; + const algorithm = options?.keyPair?.algorithm || 'HS256'; + + const signOptions: jwt.SignOptions = { + algorithm: algorithm as jwt.Algorithm, + ...(options?.keyPair?.kid && { keyid: options.keyPair.kid }) + }; + + const accessToken = jwt.sign(newPayload, signingKey, signOptions); + + return { + access_token: accessToken, + issued_token_type: request.requested_token_type || TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: expiresIn, + scope: scope + }; +}; + +/** + * Validates the token exchange request according to RFC 8693 + */ +export function validateTokenExchangeRequest(body: Record): TokenExchangeRequest { + if (!body.grant_type || body.grant_type !== TOKEN_EXCHANGE_GRANT_TYPE) { + throw new TokenExchangeError('Invalid or missing grant_type', 'invalid_request'); + } + + if (!body.subject_token) { + throw new TokenExchangeError('Missing required parameter: subject_token', 'invalid_request'); + } + + if (!body.subject_token_type) { + throw new TokenExchangeError('Missing required parameter: subject_token_type', 'invalid_request'); + } + + return { + grant_type: body.grant_type as string, + subject_token: body.subject_token as string, + subject_token_type: body.subject_token_type as string, + audience: body.audience as string | undefined, + scope: body.scope as string | undefined, + requested_token_type: body.requested_token_type as string | undefined, + resource: body.resource as string | undefined, + actor_token: body.actor_token as string | undefined, + actor_token_type: body.actor_token_type as string | undefined + }; +} + +/** + * Creates the token exchange response with proper RFC 8693 formatting + */ +export function createTokenExchangeResponse( + tokenResponse: TokenExchangeResponse, + options?: ResponseOptions, + providerName?: string +): TokenExchangeResponse { + const response: TokenExchangeResponse = { + access_token: tokenResponse.access_token, + issued_token_type: tokenResponse.issued_token_type, + token_type: tokenResponse.token_type || 'Bearer', + }; + + if (tokenResponse.expires_in || options?.expiresIn) { + response.expires_in = options?.expiresIn || tokenResponse.expires_in; + } + + if (tokenResponse.scope) { + response.scope = tokenResponse.scope; + } + + if (tokenResponse.refresh_token) { + response.refresh_token = tokenResponse.refresh_token; + } + + // Add general additional fields + if (options?.additionalFields) { + Object.assign(response, options.additionalFields); + } + + // Add provider-specific additional fields + if (providerName && options?.providerAdditionalFields?.[providerName]) { + Object.assign(response, options.providerAdditionalFields[providerName]); + } + + return response; +} + +/** + * Creates an Express middleware for handling RFC 8693 token exchange requests + * + * @param options Configuration options for token exchange + * @returns Express middleware handler + * + * @example + * ```typescript + * const app = express(); + * + * app.use('/oauth/token', customTokenExchange({ + * issuer: 'https://your-auth0-domain.auth0.com/', + * audience: 'your-api-identifier', + * keyPair: { + * privateKey: fs.readFileSync('private-key.pem'), + * publicKey: fs.readFileSync('public-key.pem'), + * algorithm: 'RS256' + * }, + * supportedTokenTypes: [TOKEN_TYPES.JWT, TOKEN_TYPES.ID_TOKEN, TOKEN_TYPES.SAML2], + * enableActorTokens: true, + * audienceScopeMapping: [{ + * sourceAudience: 'external-api', + * targetAudience: 'internal-api', + * scopeMapping: { 'read': ['internal:read', 'internal:list'] } + * }] + * })); + * ``` + */ +export function customTokenExchange(options: CustomTokenExchangeOptions): Handler { + const exchangeHandler = options.exchangeHandler || defaultTokenExchangeHandler; + + return async (req: Request, res: Response, next: NextFunction): Promise => { + try { + // Only handle POST requests to token endpoints + if (req.method !== 'POST') { + return next(); + } + + // Store options in request for access by handler + const extReq = req as Request & { + tokenExchangeOptions?: CustomTokenExchangeOptions; + body: Record; + }; + extReq.tokenExchangeOptions = options; + + // Validate the token exchange request + const exchangeRequest = validateTokenExchangeRequest(extReq.body); + + // Validate and decode the subject token with provider detection + const { payload: subjectPayload, providerName } = await validateSubjectToken( + exchangeRequest.subject_token, + exchangeRequest.subject_token_type, + options + ); + + // Validate actor token if present and enabled + let actorInfo: ActorTokenInfo | undefined; + if (options.enableActorTokens && exchangeRequest.actor_token && exchangeRequest.actor_token_type) { + try { + actorInfo = await validateActorToken( + exchangeRequest.actor_token, + exchangeRequest.actor_token_type, + options + ); + } catch (error) { + throw new TokenExchangeError('Invalid actor token', 'invalid_grant'); + } + } + + // Store token exchange data in request for potential use by other middleware + const reqWithExchange = req as Request & { + tokenExchange?: { + subjectPayload: JWTPayload; + exchangeRequest: TokenExchangeRequest; + actorInfo?: ActorTokenInfo; + providerName?: string; + }; + }; + reqWithExchange.tokenExchange = { + subjectPayload, + exchangeRequest, + actorInfo, + providerName + }; + + // Execute the token exchange handler + const tokenResponse = await exchangeHandler( + subjectPayload, + exchangeRequest, + req, + actorInfo + ); + + // Create the standardized response with provider-specific enhancements + const response = createTokenExchangeResponse( + tokenResponse, + options.responseOptions, + providerName + ); + + // Set appropriate headers according to RFC 6749 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (res as any).set({ + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache' + }); + + // Send the token exchange response + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (res as any).json(response); + + } catch (error) { + // Enhanced error handling according to RFC 6749 and RFC 8693 + let errorCode = 'invalid_request'; + let statusCode = 400; + let errorMessage = 'Invalid token exchange request'; + + if (error instanceof TokenExchangeError) { + errorCode = error.error; + errorMessage = error.message; + } else if (error instanceof InvalidSubjectTokenError) { + errorCode = 'invalid_grant'; + errorMessage = error.message; + } else if (error instanceof UnsupportedTokenTypeError) { + errorCode = 'unsupported_token_type'; + errorMessage = error.message; + } else if (error instanceof Error) { + errorMessage = error.message; + } + + // Set appropriate status code based on error type + if (errorCode === 'invalid_grant' || errorCode === 'invalid_token') { + statusCode = 401; + } else if (errorCode === 'unsupported_token_type') { + statusCode = 400; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (res as any).status(statusCode).json({ + error: errorCode, + error_description: errorMessage + }); + } + }; +} + +/** + * Utility function to create a basic Auth0-compatible token exchange middleware + * + * @param issuer Auth0 domain (e.g., 'https://your-domain.auth0.com/') + * @param audience API identifier + * @param secret JWT secret or certificate + * @returns Express middleware for token exchange + */ +export function auth0TokenExchange( + issuer: string, + audience: string, + secret: string +): Handler { + return customTokenExchange({ + issuer, + audience, + secret, + algorithms: ['RS256'], + supportedTokenTypes: [TOKEN_TYPES.ACCESS_TOKEN, TOKEN_TYPES.ID_TOKEN, TOKEN_TYPES.JWT], + enableActorTokens: true, + tokenValidation: { + clockTolerance: 60, // 1 minute tolerance + ignoreExpiration: false, + ignoreNotBefore: false + }, + responseOptions: { + issuedTokenType: TOKEN_TYPES.ACCESS_TOKEN, + expiresIn: 3600 // 1 hour + } + }); +} + +/** + * Error classes for token exchange with proper RFC mapping + */ +export class TokenExchangeError extends Error { + constructor( + message: string, + public readonly error: string = 'invalid_request' + ) { + super(message); + this.name = 'TokenExchangeError'; + } +} + +export class InvalidSubjectTokenError extends TokenExchangeError { + constructor(message: string) { + super(message, 'invalid_grant'); + this.name = 'InvalidSubjectTokenError'; + } +} + +export class UnsupportedTokenTypeError extends TokenExchangeError { + constructor(tokenType: string) { + super(`Unsupported token type: ${tokenType}`, 'unsupported_token_type'); + this.name = 'UnsupportedTokenTypeError'; + } +} \ No newline at end of file diff --git a/packages/express-oauth2-jwt-bearer/src/index.ts b/packages/express-oauth2-jwt-bearer/src/index.ts index bec779e..28b45f2 100644 --- a/packages/express-oauth2-jwt-bearer/src/index.ts +++ b/packages/express-oauth2-jwt-bearer/src/index.ts @@ -20,6 +20,7 @@ import { } from 'access-token-jwt'; import { resolveHost } from './resolve-host'; +// Extend Express Request interface to include auth property declare global { namespace Express { interface Request { @@ -80,10 +81,12 @@ export const auth = (opts: AuthOptions = {}): Handler => { assertValidDPoPOptions(opts.dpop); return async (req: Request, res: Response, next: NextFunction) => { - const { headers, query, body, method } = req; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { headers, query, body, method } = req as any; // Construct the URL from the request object. - const url = `${req.protocol}://${resolveHost(req)}${req.originalUrl ?? req.url}`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const url = `${(req as any).protocol}://${resolveHost(req)}${(req as any).originalUrl ?? (req as any).url}`; // Get DPoP verifier instance with the provided options. const requestOptions: RequestLike = { @@ -92,14 +95,16 @@ export const auth = (opts: AuthOptions = {}): Handler => { method, query, body, - isUrlEncoded: !!req.is('urlencoded'), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + isUrlEncoded: !!(req as any).is('urlencoded'), }; // Verify both JWT and DPoP token claims. const verifier = tokenVerifier(verifyJwt, opts, requestOptions); try { - req.auth = await verifier.verify(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (req as any).auth = await verifier.verify(); next(); } catch (e) { if (opts.authRequired === false) { @@ -114,9 +119,10 @@ export const auth = (opts: AuthOptions = {}): Handler => { const toHandler = (fn: (payload?: JWTPayload) => void): Handler => - (req, res, next) => { + (req: Request, res: Response, next: NextFunction) => { try { - fn(req.auth?.payload); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fn((req as any).auth?.payload); next(); } catch (e) { next(e); @@ -213,3 +219,25 @@ export { InvalidTokenError, InsufficientScopeError, } from 'oauth2-bearer'; + + +// Custom Token Exchange (RFC 8693) exports +export { + customTokenExchange, + auth0TokenExchange, + validateTokenExchangeRequest, + validateSubjectToken, + createTokenExchangeResponse, + defaultTokenExchangeHandler, + TOKEN_EXCHANGE_GRANT_TYPE, + TOKEN_TYPES, + TokenExchangeError, + InvalidSubjectTokenError, + UnsupportedTokenTypeError, + type CustomTokenExchangeOptions, + type TokenExchangeRequest, + type TokenExchangeResponse, + type TokenExchangeHandler, + type TokenValidationOptions, + type ResponseOptions +} from './custom-token-exchange'; \ No newline at end of file diff --git a/packages/express-oauth2-jwt-bearer/test/custom-token-exchange.test.ts b/packages/express-oauth2-jwt-bearer/test/custom-token-exchange.test.ts new file mode 100644 index 0000000..2b67244 --- /dev/null +++ b/packages/express-oauth2-jwt-bearer/test/custom-token-exchange.test.ts @@ -0,0 +1,996 @@ +import { Request, Response } from 'express'; +import express from 'express'; +import request from 'supertest'; +import * as jwt from 'jsonwebtoken'; + +// Mock node-fetch +jest.mock('node-fetch'); +import { + customTokenExchange, + auth0TokenExchange, + validateTokenExchangeRequest, + validateSubjectToken, + createTokenExchangeResponse, + defaultTokenExchangeHandler, + TOKEN_EXCHANGE_GRANT_TYPE, + TOKEN_TYPES, + CustomTokenExchangeOptions, + TokenExchangeRequest, + TokenExchangeResponse, + TokenExchangeHandler, + TokenExchangeError, + InvalidSubjectTokenError, + UnsupportedTokenTypeError +} from '../src/custom-token-exchange'; + +describe('Custom Token Exchange', () => { + const testSecret = 'test-secret'; + const testIssuer = 'https://test-issuer.com/'; + const testAudience = 'test-api'; + + let app: express.Application; + let validToken: string; + let expiredToken: string; + + beforeAll(() => { + // Create test tokens + const payload = { + iss: testIssuer, + aud: testAudience, + sub: 'test-user', + scope: 'read write', + exp: Math.floor(Date.now() / 1000) + 3600, + iat: Math.floor(Date.now() / 1000) + }; + + validToken = jwt.sign(payload, testSecret); + + const expiredPayload = { + ...payload, + exp: Math.floor(Date.now() / 1000) - 3600 + }; + expiredToken = jwt.sign(expiredPayload, testSecret); + }); + + beforeEach(() => { + app = express(); + (app as any).use(express.json()); + (app as any).use(express.urlencoded({ extended: true })); + }); + + describe('validateTokenExchangeRequest', () => { + it('should validate a correct token exchange request', () => { + const body = { + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN, + audience: 'new-audience', + scope: 'read' + }; + + const result = validateTokenExchangeRequest(body); + expect(result.grant_type).toBe(TOKEN_EXCHANGE_GRANT_TYPE); + expect(result.subject_token).toBe(validToken); + expect(result.audience).toBe('new-audience'); + }); + + it('should throw error for invalid grant_type', () => { + const body = { + grant_type: 'invalid', + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }; + + expect(() => validateTokenExchangeRequest(body)).toThrow('Invalid or missing grant_type'); + }); + + it('should throw error for missing subject_token', () => { + const body = { + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }; + + expect(() => validateTokenExchangeRequest(body)).toThrow('Missing required parameter: subject_token'); + }); + + it('should throw error for missing subject_token_type', () => { + const body = { + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken + }; + + expect(() => validateTokenExchangeRequest(body)).toThrow('Missing required parameter: subject_token_type'); + }); + }); + + describe('validateSubjectToken', () => { + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret, + algorithms: ['HS256'] + }; + + it('should validate a correct token', async () => { + const result = await validateSubjectToken(validToken, TOKEN_TYPES.ACCESS_TOKEN, options); + expect(result.payload.iss).toBe(testIssuer); + expect(result.payload.aud).toBe(testAudience); + expect(result.payload.sub).toBe('test-user'); + }); + + it('should reject an expired token', async () => { + await expect(validateSubjectToken(expiredToken, TOKEN_TYPES.ACCESS_TOKEN, options)) + .rejects.toThrow('Token validation failed'); + }); + + it('should reject token with wrong issuer', async () => { + const wrongIssuerOptions = { ...options, issuer: 'https://wrong.issuer.com/' }; + await expect(validateSubjectToken(validToken, TOKEN_TYPES.ACCESS_TOKEN, wrongIssuerOptions)) + .rejects.toThrow('Token validation failed'); + }); + + it('should apply custom validation', async () => { + const customOptions = { + ...options, + tokenValidation: { + customValidation: (payload: any) => payload.sub === 'allowed-user' + } + }; + + await expect(validateSubjectToken(validToken, TOKEN_TYPES.ACCESS_TOKEN, customOptions)) + .rejects.toThrow('Custom token validation failed'); + }); + + it('should pass custom validation with correct user', async () => { + const customOptions = { + ...options, + tokenValidation: { + customValidation: (payload: any) => payload.sub === 'test-user' + } + }; + + const result = await validateSubjectToken(validToken, TOKEN_TYPES.ACCESS_TOKEN, customOptions); + expect(result.payload.sub).toBe('test-user'); + }); + }); + + describe('createTokenExchangeResponse', () => { + it('should create a basic token exchange response', () => { + const tokenResponse: TokenExchangeResponse = { + access_token: 'new-token', + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: 3600, + scope: 'read write' + }; + + const result = createTokenExchangeResponse(tokenResponse); + expect(result.access_token).toBe('new-token'); + expect(result.issued_token_type).toBe(TOKEN_TYPES.ACCESS_TOKEN); + expect(result.token_type).toBe('Bearer'); + expect(result.expires_in).toBe(3600); + expect(result.scope).toBe('read write'); + }); + + it('should override expires_in with response options', () => { + const tokenResponse: TokenExchangeResponse = { + access_token: 'new-token', + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: 3600 + }; + + const result = createTokenExchangeResponse(tokenResponse, { + expiresIn: 7200 + }); + expect(result.expires_in).toBe(7200); + }); + + it('should add additional fields', () => { + const tokenResponse: TokenExchangeResponse = { + access_token: 'new-token', + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer' + }; + + const result = createTokenExchangeResponse(tokenResponse, { + additionalFields: { + custom_field: 'custom_value', + another_field: 123 + } + }); + + expect((result as any).custom_field).toBe('custom_value'); + expect((result as any).another_field).toBe(123); + }); + }); + + describe('defaultTokenExchangeHandler', () => { + it('should create a new token with modified claims', async () => { + const subjectPayload = { + iss: testIssuer, + aud: testAudience, + sub: 'test-user', + scope: 'read write', + exp: Math.floor(Date.now() / 1000) + 3600, + iat: Math.floor(Date.now() / 1000) + }; + + const exchangeRequest: TokenExchangeRequest = { + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN, + audience: 'new-audience', + scope: 'read' + }; + + const mockRequest = {} as Request; + const result = await Promise.resolve(defaultTokenExchangeHandler(subjectPayload, exchangeRequest, mockRequest)); + + expect(result.access_token).toBeDefined(); + expect(result.issued_token_type).toBe(TOKEN_TYPES.ACCESS_TOKEN); + expect(result.token_type).toBe('Bearer'); + expect(result.expires_in).toBe(3600); + expect(result.scope).toBe('read'); + }); + }); + + describe('customTokenExchange middleware', () => { + it('should handle valid token exchange request', async () => { + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret, + algorithms: ['HS256'] + }; + + (app as any).post('/token', customTokenExchange(options)); + + const response = await request(app as any) + .post('/token') + .send({ + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN, + audience: 'new-audience', + scope: 'read' + }); + + expect(response.status).toBe(200); + expect(response.body.access_token).toBeDefined(); + expect(response.body.issued_token_type).toBe(TOKEN_TYPES.ACCESS_TOKEN); + expect(response.body.token_type).toBe('Bearer'); + expect(response.body.expires_in).toBe(3600); + }); + + it('should handle custom exchange handler', async () => { + const customHandler = jest.fn().mockResolvedValue({ + access_token: 'custom-token', + issued_token_type: TOKEN_TYPES.JWT, + token_type: 'Bearer', + expires_in: 7200, + scope: 'custom-scope' + }); + + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret, + algorithms: ['HS256'], + exchangeHandler: customHandler + }; + + (app as any).post('/token', customTokenExchange(options)); + + const response = await request(app as any) + .post('/token') + .send({ + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }); + + expect(response.status).toBe(200); + expect(response.body.access_token).toBe('custom-token'); + expect(response.body.issued_token_type).toBe(TOKEN_TYPES.JWT); + expect(response.body.expires_in).toBe(7200); + expect(customHandler).toHaveBeenCalled(); + }); + + it('should return 400 for invalid grant_type', async () => { + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret + }; + + (app as any).post('/token', customTokenExchange(options)); + + const response = await request(app as any) + .post('/token') + .send({ + grant_type: 'invalid', + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('invalid_request'); + expect(response.body.error_description).toContain('Invalid or missing grant_type'); + }); + + it('should return 400 for invalid subject token', async () => { + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret + }; + + (app as any).post('/token', customTokenExchange(options)); + + const response = await request(app as any) + .post('/token') + .send({ + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: 'invalid-token', + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }); + + expect(response.status).toBe(401); + expect(response.body.error).toBe('invalid_grant'); + expect(response.body.error_description).toContain('Invalid token format'); + }); + + it('should set correct headers', async () => { + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret, + algorithms: ['HS256'] + }; + + (app as any).post('/token', customTokenExchange(options)); + + const response = await request(app as any) + .post('/token') + .send({ + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }); + + expect(response.headers['content-type']).toContain('application/json'); + expect(response.headers['cache-control']).toBe('no-store'); + expect(response.headers['pragma']).toBe('no-cache'); + }); + + it('should pass through non-POST requests', async () => { + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret + }; + + const nextHandler = jest.fn((req, res) => res.status(200).json({ message: 'passed through' })); + + (app as any).use('/token', customTokenExchange(options)); + (app as any).get('/token', nextHandler); + + const response = await request(app as any).get('/token'); + + expect(response.status).toBe(200); + expect(response.body.message).toBe('passed through'); + }); + + it('should store token exchange data in request', async () => { + let requestData: any = null; + + const customHandler = jest.fn().mockImplementation((subjectPayload, exchangeRequest, req) => { + requestData = req.tokenExchange; + return { + access_token: 'test-token', + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer' + }; + }); + + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret, + algorithms: ['HS256'], + exchangeHandler: customHandler + }; + + (app as any).post('/token', customTokenExchange(options)); + + await request(app as any) + .post('/token') + .send({ + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }); + + expect(requestData).toBeDefined(); + expect(requestData.subjectPayload).toBeDefined(); + expect(requestData.exchangeRequest).toBeDefined(); + expect(requestData.subjectPayload.sub).toBe('test-user'); + }); + }); + + describe('auth0TokenExchange', () => { + it('should create Auth0-compatible middleware', async () => { + // Create a custom Auth0-compatible middleware that accepts HS256 for testing + const testAuth0Middleware = customTokenExchange({ + issuer: testIssuer, + audience: testAudience, + secret: testSecret, + algorithms: ['HS256'], // Allow HS256 for testing + tokenValidation: { + clockTolerance: 60, + ignoreExpiration: false, + ignoreNotBefore: false + }, + responseOptions: { + issuedTokenType: TOKEN_TYPES.ACCESS_TOKEN, + expiresIn: 3600 + } + }); + + (app as any).post('/oauth/token', testAuth0Middleware); + + const response = await request(app as any) + .post('/oauth/token') + .send({ + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }); + + if (response.status !== 200) { + console.log('Error response:', response.body); + } + expect(response.status).toBe(200); + expect(response.body.access_token).toBeDefined(); + expect(response.body.issued_token_type).toBe(TOKEN_TYPES.ACCESS_TOKEN); + expect(response.body.expires_in).toBe(3600); + }); + }); + + describe('Error classes', () => { + it('should create TokenExchangeError correctly', () => { + const error = new TokenExchangeError('Test error', 'test_error'); + expect(error.message).toBe('Test error'); + expect(error.error).toBe('test_error'); + expect(error.name).toBe('TokenExchangeError'); + }); + + it('should create InvalidSubjectTokenError correctly', () => { + const error = new InvalidSubjectTokenError('Invalid token'); + expect(error.message).toBe('Invalid token'); + expect(error.error).toBe('invalid_grant'); + expect(error.name).toBe('InvalidSubjectTokenError'); + }); + + it('should create UnsupportedTokenTypeError correctly', () => { + const error = new UnsupportedTokenTypeError('custom_type'); + expect(error.message).toBe('Unsupported token type: custom_type'); + expect(error.error).toBe('unsupported_token_type'); + expect(error.name).toBe('UnsupportedTokenTypeError'); + }); + }); + + describe('Integration tests', () => { + it('should handle complete token exchange flow', async () => { + const customHandler = async (subjectPayload: any, exchangeRequest: TokenExchangeRequest) => { + // Simulate custom business logic + const { exp, ...payloadWithoutExp } = subjectPayload; + const newPayload = { + ...payloadWithoutExp, + aud: exchangeRequest.audience || subjectPayload.aud, + scope: exchangeRequest.scope || subjectPayload.scope, + custom_claim: 'added_by_exchange' + }; + + const newToken = jwt.sign(newPayload, testSecret, { expiresIn: '2h' }); + + return { + access_token: newToken, + issued_token_type: TOKEN_TYPES.ACCESS_TOKEN, + token_type: 'Bearer', + expires_in: 7200, + scope: exchangeRequest.scope + }; + }; + + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret, + algorithms: ['HS256'], + exchangeHandler: customHandler + }; + + (app as any).post('/oauth/token', customTokenExchange(options)); + + const response = await request(app as any) + .post('/oauth/token') + .send({ + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN, + audience: 'https://api.example.com', + scope: 'read:data write:data' + }); + + if (response.status !== 200) { + console.log('Integration test error response:', response.body); + } + expect(response.status).toBe(200); + expect(response.body.access_token).toBeDefined(); + expect(response.body.expires_in).toBe(7200); + expect(response.body.scope).toBe('read:data write:data'); + + // Verify the new token contains expected claims + const decodedToken = jwt.verify(response.body.access_token, testSecret) as any; + expect(decodedToken.aud).toBe('https://api.example.com'); + expect(decodedToken.scope).toBe('read:data write:data'); + expect(decodedToken.custom_claim).toBe('added_by_exchange'); + }); + + it('should handle token exchange with response options', async () => { + const options: CustomTokenExchangeOptions = { + issuer: testIssuer, + audience: testAudience, + secret: testSecret, + algorithms: ['HS256'], + responseOptions: { + expiresIn: 1800, + additionalFields: { + refresh_token: 'refresh_token_value', + custom_field: 'custom_value' + } + } + }; + + (app as any).post('/oauth/token', customTokenExchange(options)); + + const response = await request(app as any) + .post('/oauth/token') + .send({ + grant_type: TOKEN_EXCHANGE_GRANT_TYPE, + subject_token: validToken, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }); + + expect(response.status).toBe(200); + expect(response.body.expires_in).toBe(1800); + expect(response.body.refresh_token).toBe('refresh_token_value'); + expect(response.body.custom_field).toBe('custom_value'); + }); + }); + + describe('Coverage Enhancement Tests', () => { + describe('Provider Custom Validators', () => { + it('should use provider custom validator when available', async () => { + const customPayload = { sub: 'custom-user', iss: 'https://custom-provider.com', aud: 'test-audience' }; + const customValidator = jest.fn().mockResolvedValue(customPayload); + + const options: CustomTokenExchangeOptions = { + issuer: 'https://custom-provider.com', + secret: 'test-secret', + audience: 'test-audience', + providers: [{ + name: 'custom-provider', + issuerPattern: /^https:\/\/custom-provider\.com\/?$/, + algorithms: ['RS256'], + customValidator + }] + }; + + const token = jwt.sign(customPayload, 'test-secret'); + const result = await validateSubjectToken(token, TOKEN_TYPES.JWT, options); + + expect(customValidator).toHaveBeenCalledWith(token); + expect(result.payload).toEqual(customPayload); + expect(result.providerName).toBe('custom-provider'); + }); + }); + + describe('Actor Token Error Handling', () => { + it('should handle invalid actor token gracefully', async () => { + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + app.use('/token', customTokenExchange({ + issuer: 'https://example.com', + secret: 'test-secret', + audience: 'test-audience', + algorithms: ['HS256'], + enableActorTokens: true + })); + + const validToken = jwt.sign({ + sub: 'user123', + iss: 'https://example.com', + aud: 'test-audience' + }, 'test-secret'); + + const response = await request(app) + .post('/token') + .send({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + subject_token: validToken, + subject_token_type: TOKEN_TYPES.JWT, + actor_token: 'invalid-actor-token', + actor_token_type: TOKEN_TYPES.JWT + }); + + expect(response.status).toBe(401); + expect(response.body.error).toBe('invalid_grant'); + expect(response.body.error_description).toContain('Invalid actor token'); + }); + }); + + describe('Additional Fields and Provider-Specific Fields', () => { + it('should include general additional fields in response', async () => { + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + app.use('/token', customTokenExchange({ + issuer: 'https://example.com', + secret: 'test-secret', + audience: 'test-audience', + algorithms: ['HS256'], + responseOptions: { + additionalFields: { + custom_field: 'global_value', + expires_in: 7200 + } + } + })); + + const token = jwt.sign({ + sub: 'user123', + iss: 'https://example.com', + aud: 'test-audience' + }, 'test-secret'); + + const response = await request(app) + .post('/token') + .send({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + subject_token: token, + subject_token_type: TOKEN_TYPES.JWT + }); + + expect(response.status).toBe(200); + expect(response.body.custom_field).toBe('global_value'); + expect(response.body.expires_in).toBe(7200); + }); + + it('should include provider-specific additional fields', async () => { + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + app.use('/token', customTokenExchange({ + issuer: 'https://test.auth0.com', + secret: 'test-secret', + audience: 'test-audience', + algorithms: ['HS256'], + providers: [{ + name: 'auth0', + issuerPattern: /^https:\/\/[a-zA-Z0-9\-]+\.auth0\.com\/?$/, + algorithms: ['HS256'] + }], + responseOptions: { + providerAdditionalFields: { + auth0: { + auth0_specific: 'auth0_value', + connection: 'Username-Password-Authentication' + } + } + } + })); + + const token = jwt.sign({ + sub: 'user123', + iss: 'https://test.auth0.com', + aud: 'test-audience' + }, 'test-secret', { algorithm: 'HS256' }); + + const response = await request(app) + .post('/token') + .send({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + subject_token: token, + subject_token_type: TOKEN_TYPES.JWT + }); + + if (response.status !== 200) { + console.log('Provider-specific fields test error:', response.body); + } + expect(response.status).toBe(200); + expect(response.body.auth0_specific).toBe('auth0_value'); + expect(response.body.connection).toBe('Username-Password-Authentication'); + }); + }); + + describe('Enhanced Error Handling', () => { + it('should handle InvalidSubjectTokenError with proper error code', async () => { + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + app.use('/token', customTokenExchange({ + issuer: 'https://example.com', + secret: 'test-secret', + audience: 'test-audience', + algorithms: ['HS256'] + })); + + const response = await request(app) + .post('/token') + .send({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + subject_token: 'completely-invalid-token', + subject_token_type: TOKEN_TYPES.JWT + }); + + expect(response.status).toBe(401); + expect(response.body.error).toBe('invalid_grant'); + }); + + it('should handle UnsupportedTokenTypeError with proper error code', async () => { + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + app.use('/token', customTokenExchange({ + issuer: 'https://example.com', + secret: 'test-secret', + audience: 'test-audience', + algorithms: ['HS256'], + supportedTokenTypes: [TOKEN_TYPES.JWT] // Only JWT supported + })); + + const token = jwt.sign({ + sub: 'user123', + iss: 'https://example.com', + aud: 'test-audience' + }, 'test-secret'); + + const response = await request(app) + .post('/token') + .send({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + subject_token: token, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN // Different from supported type + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('unsupported_token_type'); + }); + + it('should handle generic Error with default error code', async () => { + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + + // Mock the exchangeHandler to throw a generic error + const mockHandler: TokenExchangeHandler = () => { + throw new Error('Generic processing error'); + }; + + app.use('/token', customTokenExchange({ + issuer: 'https://example.com', + secret: 'test-secret', + audience: 'test-audience', + algorithms: ['HS256'], + exchangeHandler: mockHandler + })); + + const token = jwt.sign({ + sub: 'user123', + iss: 'https://example.com', + aud: 'test-audience' + }, 'test-secret'); + + const response = await request(app) + .post('/token') + .send({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + subject_token: token, + subject_token_type: TOKEN_TYPES.JWT + }); + + expect(response.status).toBe(400); + expect(response.body.error).toBe('invalid_request'); + expect(response.body.error_description).toBe('Generic processing error'); + }); + }); + + describe('Token Introspection', () => { + let mockFetch: any; + + beforeEach(() => { + // Get the mocked fetch from node-fetch + mockFetch = require('node-fetch'); + jest.clearAllMocks(); + }); + + it('should validate token via introspection when configured', async () => { + // Mock fetch for introspection + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + active: true, + sub: 'user123', + iss: 'https://external-provider.com', + aud: 'test-audience', + exp: Math.floor(Date.now() / 1000) + 3600 + }) + }); + + const options: CustomTokenExchangeOptions = { + issuer: 'https://external-provider.com', + secret: 'test-secret', + audience: 'test-audience', + algorithms: ['HS256'], + providers: [{ + name: 'external-provider', + issuerPattern: /^https:\/\/external-provider\.com\/?$/, + algorithms: ['HS256'], + introspectionEndpoint: 'https://external-provider.com/introspect' + }], + tokenValidation: { + useIntrospection: true + }, + introspection: { + endpoint: 'https://external-provider.com/introspect', + clientId: 'client-id', + clientSecret: 'client-secret' + } + }; + + // Create a token with external issuer to trigger introspection + const token = jwt.sign({ + sub: 'user123', + iss: 'https://external-provider.com', + aud: 'test-audience' + }, 'test-secret'); + const result = await validateSubjectToken(token, TOKEN_TYPES.ACCESS_TOKEN, options); + + expect(mockFetch).toHaveBeenCalledWith( + 'https://external-provider.com/introspect', + expect.objectContaining({ + method: 'POST', + headers: expect.objectContaining({ + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': expect.any(String) + }), + body: expect.any(String) + }) + ); + + expect(result.payload.sub).toBe('user123'); + expect(result.providerName).toBe('external-provider'); + }); + + it('should handle inactive introspection response', async () => { + mockFetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ active: false }) + }); + + const options: CustomTokenExchangeOptions = { + issuer: 'https://external-provider.com', + secret: 'test-secret', + audience: 'test-audience', + algorithms: ['HS256'], + providers: [{ + name: 'external-provider', + issuerPattern: /^https:\/\/external-provider\.com\/?$/, + algorithms: ['HS256'], + introspectionEndpoint: 'https://external-provider.com/introspect' + }], + tokenValidation: { + useIntrospection: true + }, + introspection: { + endpoint: 'https://external-provider.com/introspect', + clientId: 'client-id', + clientSecret: 'client-secret' + } + }; + + const inactiveToken = jwt.sign({ + sub: 'inactive-user', + iss: 'https://external-provider.com', + aud: 'test-audience' + }, 'test-secret'); + + await expect( + validateSubjectToken(inactiveToken, TOKEN_TYPES.ACCESS_TOKEN, options) + ).rejects.toThrow('Token is not active'); + }); + }); + + describe('Auth0 Convenience Function', () => { + it('should create middleware with auth0-specific configuration', async () => { + const middleware = auth0TokenExchange( + 'https://test.auth0.com', + 'test-audience', + 'test-secret' + ); + + // Verify it returns a function (middleware) + expect(typeof middleware).toBe('function'); + + // Test the middleware with a request - use HS256 for testing + const testMiddleware = customTokenExchange({ + issuer: 'https://test.auth0.com', + audience: 'test-audience', + secret: 'test-secret', + algorithms: ['HS256'], // Override to HS256 for testing + supportedTokenTypes: [TOKEN_TYPES.ACCESS_TOKEN, TOKEN_TYPES.ID_TOKEN, TOKEN_TYPES.JWT], + enableActorTokens: true, + tokenValidation: { + clockTolerance: 60, + ignoreExpiration: false, + ignoreNotBefore: false + }, + responseOptions: { + issuedTokenType: TOKEN_TYPES.ACCESS_TOKEN, + expiresIn: 3600 + } + }); + + const app = express(); + app.use(express.json()); + app.use(express.urlencoded({ extended: true })); + app.use('/token', testMiddleware); + + const token = jwt.sign({ + sub: 'user123', + iss: 'https://test.auth0.com', + aud: 'test-audience' + }, 'test-secret', { algorithm: 'HS256' }); + + const response = await request(app) + .post('/token') + .send({ + grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange', + subject_token: token, + subject_token_type: TOKEN_TYPES.ACCESS_TOKEN + }); + + if (response.status !== 200) { + console.log('Auth0 convenience function test error:', response.body); + } + expect(response.status).toBe(200); + expect(response.body.access_token).toBeDefined(); + expect(response.body.token_type).toBe('Bearer'); + }); + }); + + describe('Missing Secret/Key Error Handling', () => { + it('should reject when no secret or public key is provided', async () => { + const options: CustomTokenExchangeOptions = { + issuer: 'https://example.com', + // No secret or keyPair provided + audience: 'test-audience' + }; + + const token = jwt.sign({ + sub: 'user123', + iss: 'https://example.com', + aud: 'test-audience' + }, 'test-secret'); + + await expect( + validateSubjectToken(token, TOKEN_TYPES.JWT, options) + ).rejects.toThrow('No secret or public key provided for token validation'); + }); + }); + }); +}); diff --git a/packages/express-oauth2-jwt-bearer/test/index.test.ts b/packages/express-oauth2-jwt-bearer/test/index.test.ts index 389891b..d361e16 100644 --- a/packages/express-oauth2-jwt-bearer/test/index.test.ts +++ b/packages/express-oauth2-jwt-bearer/test/index.test.ts @@ -6,6 +6,15 @@ import express from 'express'; import nock from 'nock'; import got, { CancelableRequest } from 'got'; import { createJwt } from 'access-token-jwt/test/helpers'; + +// Jest matcher extensions +declare global { + namespace jest { + interface Expect { + objectContaining(sample: Record): any; + } + } +} import { auth, AuthOptions, @@ -29,8 +38,11 @@ const expectFailsWith = async ( ) => { try { await promise; - fail('Request should fail'); - } catch (e) { + throw new Error('Request should fail'); + } catch (e: any) { + if (e.message === 'Request should fail') { + throw e; + } const error = code ? `, error="${code}"` : ''; const errorDescription = description ? `, error_description="${description}"`