From 452cda78c734f237b8befce30ce5d9d8d1ac8d38 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Mon, 15 Jun 2026 10:30:18 +0200 Subject: [PATCH 01/11] feature(dev): replace eslint with biome toolchain --- .eslintignore | 5 - .eslintrc | 54 - biome.json | 38 + package-lock.json | 3189 ++++++++++++++++----------------------------- package.json | 16 +- 5 files changed, 1200 insertions(+), 2102 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 biome.json diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 0d92402c..00000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -.github -.nyc_output -coverage -docs/* -node_modules diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 8e36b31a..00000000 --- a/.eslintrc +++ /dev/null @@ -1,54 +0,0 @@ -{ - "extends": "eslint:recommended", - "env": { - "node": true, - "mocha": true, - "es6": true - }, - "parserOptions": { - "ecmaVersion": 9, - "sourceType": "module", - "ecmaFeatures" : { - "globalReturn": false, - "impliedStrict": true, - "jsx": false - } - }, - "rules": { - "indent": [ - "error", - 2 - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ], - "no-console": [ - "error" - ], - "no-var": [ - "error" - ], - "prefer-const": ["error", { - "destructuring": "any", - "ignoreReadBeforeAssign": false - }], - "no-unused-vars": [ - "error", - { - "vars": "all", - "args": "none", - "ignoreRestSiblings": false - } - ] - } -} - \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..56027c5e --- /dev/null +++ b/biome.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "lib/**/*.js", + "test/**/*.js" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/package-lock.json b/package-lock.json index 5dd65b98..f83351ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,16 @@ "dependencies": { "@node-oauth/formats": "1.0.0", "basic-auth": "2.0.1", - "type-is": "2.0.1" + "type-is": "^2.1.0" }, "devDependencies": { + "@biomejs/biome": "2.5.0", "chai": "6.2.2", - "eslint": "8.57.1", "jsdoc-to-markdown": "^9.1.3", - "mocha": "11.7.5", + "mocha": "^11.7.6", "nyc": "18.0.0", - "sinon": "21.0.3", - "vitepress": "^2.0.0-alpha.15" + "sinon": "^22.0.0", + "vitepress": "^2.0.0-alpha.17" }, "engines": { "node": ">=16.0.0" @@ -243,9 +243,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -253,9 +253,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -286,13 +286,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -347,19 +347,182 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.0.tgz", + "integrity": "sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.5.0", + "@biomejs/cli-darwin-x64": "2.5.0", + "@biomejs/cli-linux-arm64": "2.5.0", + "@biomejs/cli-linux-arm64-musl": "2.5.0", + "@biomejs/cli-linux-x64": "2.5.0", + "@biomejs/cli-linux-x64-musl": "2.5.0", + "@biomejs/cli-win32-arm64": "2.5.0", + "@biomejs/cli-win32-x64": "2.5.0" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz", + "integrity": "sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.0.tgz", + "integrity": "sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.0.tgz", + "integrity": "sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.0.tgz", + "integrity": "sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.0.tgz", + "integrity": "sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@docsearch/css": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.5.3.tgz", @@ -379,9 +542,9 @@ "dev": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -396,9 +559,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -413,9 +576,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -430,9 +593,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -447,9 +610,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -464,9 +627,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -481,9 +644,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -498,9 +661,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -515,9 +678,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -532,9 +695,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -549,9 +712,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -566,9 +729,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -583,9 +746,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -600,9 +763,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -617,9 +780,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -634,9 +797,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -651,9 +814,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -668,9 +831,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -685,9 +848,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -702,9 +865,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -719,9 +882,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -736,9 +899,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -753,9 +916,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -770,9 +933,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -787,9 +950,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -804,9 +967,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -820,97 +983,6 @@ "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true - }, "node_modules/@iconify-json/simple-icons": { "version": "1.2.69", "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.69.tgz", @@ -1243,332 +1315,357 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openharmony" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1656,24 +1753,27 @@ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", - "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "node_modules/@sinonjs/samsam": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-9.0.3.tgz", - "integrity": "sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", "type-detect": "^4.1.0" @@ -1684,14 +1784,15 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1758,70 +1859,74 @@ "dev": true }, "node_modules/@vitejs/plugin-vue": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", - "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.53" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "node_modules/@vue/compiler-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", - "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz", + "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.27", - "entities": "^7.0.0", + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.38", + "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", - "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz", + "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-core": "3.5.38", + "@vue/shared": "3.5.38" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", - "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz", + "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.27", - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27", + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.38", + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.6", + "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", - "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz", + "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.38", + "@vue/shared": "3.5.38" } }, "node_modules/@vue/devtools-api": { @@ -1861,65 +1966,70 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", - "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz", + "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/shared": "3.5.27" + "@vue/shared": "3.5.38" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", - "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz", + "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/reactivity": "3.5.38", + "@vue/shared": "3.5.38" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", - "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz", + "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.27", - "@vue/runtime-core": "3.5.27", - "@vue/shared": "3.5.27", + "@vue/reactivity": "3.5.38", + "@vue/runtime-core": "3.5.38", + "@vue/shared": "3.5.38", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", - "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz", + "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38" }, "peerDependencies": { - "vue": "3.5.27" + "vue": "3.5.38" } }, "node_modules/@vue/shared": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", - "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", - "dev": true + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz", + "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", + "dev": true, + "license": "MIT" }, "node_modules/@vueuse/core": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz", - "integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", "dev": true, "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.1.0", - "@vueuse/shared": "14.1.0" + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -1929,14 +2039,14 @@ } }, "node_modules/@vueuse/integrations": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.1.0.tgz", - "integrity": "sha512-eNQPdisnO9SvdydTIXnTE7c29yOsJBD/xkwEyQLdhDC/LKbqrFpXHb3uS//7NcIrQO3fWVuvMGp8dbK6mNEMCA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.3.0.tgz", + "integrity": "sha512-76I5FT2ESvCmCaSwapI+a/u/CFtNXmzl9f9lNp1hRtx8vKB8hfiokJr8IvQqcQG5ckGXElyXK516b54ozV3MvA==", "dev": true, "license": "MIT", "dependencies": { - "@vueuse/core": "14.1.0", - "@vueuse/shared": "14.1.0" + "@vueuse/core": "14.3.0", + "@vueuse/shared": "14.3.0" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -1946,7 +2056,7 @@ "axios": "^1", "change-case": "^5", "drauu": "^0.4", - "focus-trap": "^7", + "focus-trap": "^7 || ^8", "fuse.js": "^7", "idb-keyval": "^6", "jwt-decode": "^4", @@ -1996,9 +2106,9 @@ } }, "node_modules/@vueuse/metadata": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz", - "integrity": "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", "dev": true, "license": "MIT", "funding": { @@ -2006,9 +2116,9 @@ } }, "node_modules/@vueuse/shared": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.1.0.tgz", - "integrity": "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", "dev": true, "license": "MIT", "funding": { @@ -2018,27 +2128,6 @@ "vue": "^3.5.0" } }, - "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -2052,22 +2141,6 @@ "node": ">=8" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2160,16 +2233,6 @@ "dev": true, "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2257,15 +2320,6 @@ "node": ">=8" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -2503,12 +2557,6 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, "node_modules/config-master": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/config-master/-/config-master-3.1.0.tgz", @@ -2530,12 +2578,16 @@ } }, "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/convert-source-map": { @@ -2579,7 +2631,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/current-module-paths": { "version": "1.1.3", @@ -2617,12 +2670,6 @@ "node": ">=0.10.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, "node_modules/default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -2710,18 +2757,6 @@ "node": ">= 12" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2745,6 +2780,7 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -2759,9 +2795,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2772,32 +2808,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -2821,106 +2857,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -2931,62 +2867,15 @@ "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", @@ -3018,18 +2907,6 @@ "node": ">= 6" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3057,18 +2934,6 @@ } } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, "node_modules/file-set": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/file-set/-/file-set-5.3.0.tgz", @@ -3164,31 +3029,12 @@ "flat": "cli.js" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, "node_modules/focus-trap": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", - "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.1.tgz", + "integrity": "sha512-6CxwrrFRquH7pDXb1mWxudkU9LSfYBMRZutpgddb2o6iwCk7cIRrBhyY3c8SGKcmIKdeMTrGSNg4Bedh2RSF/w==", "dev": true, + "license": "MIT", "dependencies": { "tabbable": "^6.4.0" } @@ -3226,12 +3072,6 @@ } ] }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3294,23 +3134,12 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3358,33 +3187,12 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/handlebars": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", @@ -3508,31 +3316,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3551,23 +3334,6 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3722,17 +3488,17 @@ } }, "node_modules/istanbul-lib-processinfo": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-3.0.0.tgz", - "integrity": "sha512-P7nLXRRlo7Sqinty6lNa7+4o9jBUYGpqtejqCOZKfgXlRoxY/QArflcB86YO500Ahj4pDJEG34JjMRbQgePLnQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-3.0.1.tgz", + "integrity": "sha512-s3mX05h5wGZeScG6XnOanygPh4SJu5ujMc9YbvpnLGXWy1cRiGbp0NdVcjHxgoZt3WfQppfBsa0y+gWdYJ2pGQ==", "dev": true, + "license": "ISC", "dependencies": { "archy": "^1.0.0", "cross-spawn": "^7.0.3", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", - "rimraf": "^6.1.3", - "uuid": "^8.3.2" + "rimraf": "^6.1.3" }, "engines": { "node": "20 || >=22" @@ -3748,10 +3514,11 @@ } }, "node_modules/istanbul-lib-processinfo/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -4073,24 +3840,6 @@ "node": ">=4" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4103,15 +3852,6 @@ "node": ">=6" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, "node_modules/klaw": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", @@ -4122,24 +3862,21 @@ "graceful-fs": "^4.1.9" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -4161,10 +3898,11 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "dev": true + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", @@ -4179,12 +3917,6 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -4215,6 +3947,7 @@ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } @@ -4251,15 +3984,25 @@ "license": "MIT" }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -4443,9 +4186,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -4476,18 +4219,6 @@ "node": ">= 0.6" } }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -4535,10 +4266,11 @@ } }, "node_modules/mocha": { - "version": "11.7.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", - "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "version": "11.7.6", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", + "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", "dev": true, + "license": "MIT", "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", @@ -4571,9 +4303,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -4623,9 +4355,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -4641,12 +4373,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -4723,10 +4449,11 @@ } }, "node_modules/nyc/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -4975,15 +4702,6 @@ "node": ">=8.0.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/oniguruma-parser": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", @@ -5001,23 +4719,6 @@ "regex-recursion": "^6.0.2" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5090,18 +4791,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5111,15 +4800,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5166,9 +4846,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -5243,9 +4923,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -5263,7 +4943,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5271,15 +4951,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -5302,15 +4973,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -5425,15 +5087,6 @@ "lodash": "^4.17.21" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5451,50 +5104,14 @@ "dev": true, "license": "MIT" }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -5504,31 +5121,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", "fsevents": "~2.3.2" } }, @@ -5631,16 +5248,16 @@ "dev": true }, "node_modules/sinon": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.3.tgz", - "integrity": "sha512-0x8TQFr8EjADhSME01u1ZK31yv2+bd6Z5NrBCHVM+n4qL1wFqbxftmeyi3bwlr49FbbzRfrqSFOpyHCOh/YmYA==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-22.0.0.tgz", + "integrity": "sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^15.1.1", - "@sinonjs/samsam": "^9.0.3", - "diff": "^8.0.3", - "supports-color": "^7.2.0" + "@sinonjs/fake-timers": "^15.4.0", + "@sinonjs/samsam": "^10.0.2", + "diff": "^9.0.0" }, "funding": { "type": "opencollective", @@ -5648,10 +5265,11 @@ } }, "node_modules/sinon/node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -5735,10 +5353,11 @@ } }, "node_modules/spawn-wrap/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -5956,7 +5575,8 @@ "version": "6.4.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/table-layout": { "version": "4.1.1", @@ -5996,10 +5616,11 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -6064,21 +5685,15 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -6110,51 +5725,32 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typedarray-to-buffer": { @@ -6198,9 +5794,9 @@ } }, "node_modules/underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true, "license": "MIT" }, @@ -6302,24 +5898,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -6349,10 +5927,11 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6423,28 +6002,29 @@ } }, "node_modules/vitepress": { - "version": "2.0.0-alpha.16", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.16.tgz", - "integrity": "sha512-w1nwsefDVIsje7BZr2tsKxkZutDGjG0YoQ2yxO7+a9tvYVqfljYbwj5LMYkPy8Tb7YbPwa22HtIhk62jbrvuEQ==", + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.17.tgz", + "integrity": "sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==", "dev": true, + "license": "MIT", "dependencies": { "@docsearch/css": "^4.5.3", "@docsearch/js": "^4.5.3", "@docsearch/sidepanel-js": "^4.5.3", - "@iconify-json/simple-icons": "^1.2.68", - "@shikijs/core": "^3.21.0", - "@shikijs/transformers": "^3.21.0", - "@shikijs/types": "^3.21.0", + "@iconify-json/simple-icons": "^1.2.69", + "@shikijs/core": "^3.22.0", + "@shikijs/transformers": "^3.22.0", + "@shikijs/types": "^3.22.0", "@types/markdown-it": "^14.1.2", - "@vitejs/plugin-vue": "^6.0.3", + "@vitejs/plugin-vue": "^6.0.4", "@vue/devtools-api": "^8.0.5", "@vue/shared": "^3.5.27", - "@vueuse/core": "^14.1.0", - "@vueuse/integrations": "^14.1.0", - "focus-trap": "^7.8.0", + "@vueuse/core": "^14.2.0", + "@vueuse/integrations": "^14.2.0", + "focus-trap": "^8.0.0", "mark.js": "8.11.1", "minisearch": "^7.2.0", - "shiki": "^3.21.0", + "shiki": "^3.22.0", "vite": "^7.3.1", "vue": "^3.5.27" }, @@ -6469,16 +6049,17 @@ } }, "node_modules/vue": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", - "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz", + "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==", "dev": true, + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-sfc": "3.5.27", - "@vue/runtime-dom": "3.5.27", - "@vue/server-renderer": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-sfc": "3.5.38", + "@vue/runtime-dom": "3.5.38", + "@vue/server-renderer": "3.5.38", + "@vue/shared": "3.5.38" }, "peerDependencies": { "typescript": "*" @@ -6520,15 +6101,6 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -6589,12 +6161,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, "node_modules/write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", @@ -6890,15 +6456,15 @@ } }, "@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true }, "@babel/helper-validator-option": { @@ -6918,12 +6484,12 @@ } }, "@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "requires": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.7" } }, "@babel/template": { @@ -6963,16 +6529,88 @@ } } }, - "@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + } + }, + "@biomejs/biome": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.0.tgz", + "integrity": "sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@biomejs/cli-darwin-arm64": "2.5.0", + "@biomejs/cli-darwin-x64": "2.5.0", + "@biomejs/cli-linux-arm64": "2.5.0", + "@biomejs/cli-linux-arm64-musl": "2.5.0", + "@biomejs/cli-linux-x64": "2.5.0", + "@biomejs/cli-linux-x64-musl": "2.5.0", + "@biomejs/cli-win32-arm64": "2.5.0", + "@biomejs/cli-win32-x64": "2.5.0" } }, + "@biomejs/cli-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==", + "dev": true, + "optional": true + }, + "@biomejs/cli-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.0.tgz", + "integrity": "sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.0.tgz", + "integrity": "sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.0.tgz", + "integrity": "sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.0.tgz", + "integrity": "sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==", + "dev": true, + "optional": true + }, + "@biomejs/cli-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.0.tgz", + "integrity": "sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==", + "dev": true, + "optional": true + }, "@docsearch/css": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.5.3.tgz", @@ -6992,248 +6630,187 @@ "dev": true }, "@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "dev": true, "optional": true }, "@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "dev": true, "optional": true }, "@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "dev": true, "optional": true }, "@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "dev": true, "optional": true }, - "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true - }, - "@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true - }, - "@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true - }, "@iconify-json/simple-icons": { "version": "1.2.69", "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.69.tgz", @@ -7484,183 +7061,183 @@ "optional": true }, "@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true }, "@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", "dev": true, "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", "dev": true, "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", "dev": true, "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", "dev": true, "optional": true }, "@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", "dev": true, "optional": true }, "@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", "dev": true, "optional": true }, "@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", "dev": true, "optional": true }, "@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", "dev": true, "optional": true }, "@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", "dev": true, "optional": true }, "@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", "dev": true, "optional": true }, "@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", "dev": true, "optional": true }, "@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", "dev": true, "optional": true }, "@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", "dev": true, "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", "dev": true, "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", "dev": true, "optional": true }, "@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", "dev": true, "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", "dev": true, "optional": true }, @@ -7751,18 +7328,18 @@ } }, "@sinonjs/fake-timers": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", - "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.1" } }, "@sinonjs/samsam": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-9.0.3.tgz", - "integrity": "sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-10.0.2.tgz", + "integrity": "sha512-8lVwD1Df1BmzoaOLhMcGGcz/Jyr5QY2KSB75/YK1QgKzoabTeLdIVyhXNZK9ojfSKSdirbXqdbsXXqP9/Ve8+A==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.1", @@ -7778,9 +7355,9 @@ } }, "@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true }, "@types/hast": { @@ -7842,62 +7419,62 @@ "dev": true }, "@vitejs/plugin-vue": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", - "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", "dev": true, "requires": { - "@rolldown/pluginutils": "1.0.0-beta.53" + "@rolldown/pluginutils": "^1.0.1" } }, "@vue/compiler-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", - "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz", + "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==", "dev": true, "requires": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.27", - "entities": "^7.0.0", + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.38", + "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "@vue/compiler-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", - "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz", + "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==", "dev": true, "requires": { - "@vue/compiler-core": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-core": "3.5.38", + "@vue/shared": "3.5.38" } }, "@vue/compiler-sfc": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", - "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz", + "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==", "dev": true, "requires": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.27", - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27", + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.38", + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.6", + "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "@vue/compiler-ssr": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", - "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz", + "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==", "dev": true, "requires": { - "@vue/compiler-dom": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.38", + "@vue/shared": "3.5.38" } }, "@vue/devtools-api": { @@ -7934,96 +7511,83 @@ } }, "@vue/reactivity": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", - "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz", + "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", "dev": true, "requires": { - "@vue/shared": "3.5.27" + "@vue/shared": "3.5.38" } }, "@vue/runtime-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", - "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz", + "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==", "dev": true, "requires": { - "@vue/reactivity": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/reactivity": "3.5.38", + "@vue/shared": "3.5.38" } }, "@vue/runtime-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", - "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz", + "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==", "dev": true, "requires": { - "@vue/reactivity": "3.5.27", - "@vue/runtime-core": "3.5.27", - "@vue/shared": "3.5.27", + "@vue/reactivity": "3.5.38", + "@vue/runtime-core": "3.5.38", + "@vue/shared": "3.5.38", "csstype": "^3.2.3" } }, "@vue/server-renderer": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", - "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz", + "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==", "dev": true, "requires": { - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38" } }, "@vue/shared": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", - "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz", + "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", "dev": true }, "@vueuse/core": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz", - "integrity": "sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", "dev": true, "requires": { "@types/web-bluetooth": "^0.0.21", - "@vueuse/metadata": "14.1.0", - "@vueuse/shared": "14.1.0" + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" } }, "@vueuse/integrations": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.1.0.tgz", - "integrity": "sha512-eNQPdisnO9SvdydTIXnTE7c29yOsJBD/xkwEyQLdhDC/LKbqrFpXHb3uS//7NcIrQO3fWVuvMGp8dbK6mNEMCA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.3.0.tgz", + "integrity": "sha512-76I5FT2ESvCmCaSwapI+a/u/CFtNXmzl9f9lNp1hRtx8vKB8hfiokJr8IvQqcQG5ckGXElyXK516b54ozV3MvA==", "dev": true, "requires": { - "@vueuse/core": "14.1.0", - "@vueuse/shared": "14.1.0" + "@vueuse/core": "14.3.0", + "@vueuse/shared": "14.3.0" } }, "@vueuse/metadata": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.1.0.tgz", - "integrity": "sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", "dev": true }, "@vueuse/shared": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.1.0.tgz", - "integrity": "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==", - "dev": true, - "requires": {} - }, - "acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", "dev": true, "requires": {} }, @@ -8037,18 +7601,6 @@ "indent-string": "^4.0.0" } }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -8117,16 +7669,6 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "dev": true }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -8175,12 +7717,6 @@ "write-file-atomic": "^3.0.0" } }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -8328,12 +7864,6 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, "config-master": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/config-master/-/config-master-3.1.0.tgz", @@ -8352,9 +7882,9 @@ } }, "content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==" }, "convert-source-map": { "version": "1.9.0", @@ -8409,12 +7939,6 @@ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, "default-require-extensions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", @@ -8468,15 +7992,6 @@ } } }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -8508,37 +8023,37 @@ "dev": true }, "esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "escalade": { @@ -8553,127 +8068,18 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true }, - "eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - } - }, - "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true - }, - "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "requires": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true }, - "esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, "estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, "fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -8698,18 +8104,6 @@ } } }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, "fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -8726,15 +8120,6 @@ "dev": true, "requires": {} }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, "file-set": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/file-set/-/file-set-5.3.0.tgz", @@ -8788,27 +8173,10 @@ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true }, - "flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "requires": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, "focus-trap": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", - "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.1.tgz", + "integrity": "sha512-6CxwrrFRquH7pDXb1mWxudkU9LSfYBMRZutpgddb2o6iwCk7cIRrBhyY3c8SGKcmIKdeMTrGSNg4Bedh2RSF/w==", "dev": true, "requires": { "tabbable": "^6.4.0" @@ -8830,12 +8198,6 @@ "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", "dev": true }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -8876,9 +8238,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -8911,36 +8273,12 @@ } } }, - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - }, - "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "handlebars": { "version": "4.7.9", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", @@ -9030,22 +8368,6 @@ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", "dev": true }, - "ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -9058,22 +8380,6 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9178,17 +8484,16 @@ } }, "istanbul-lib-processinfo": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-3.0.0.tgz", - "integrity": "sha512-P7nLXRRlo7Sqinty6lNa7+4o9jBUYGpqtejqCOZKfgXlRoxY/QArflcB86YO500Ahj4pDJEG34JjMRbQgePLnQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-3.0.1.tgz", + "integrity": "sha512-s3mX05h5wGZeScG6XnOanygPh4SJu5ujMc9YbvpnLGXWy1cRiGbp0NdVcjHxgoZt3WfQppfBsa0y+gWdYJ2pGQ==", "dev": true, "requires": { "archy": "^1.0.0", "cross-spawn": "^7.0.3", "istanbul-lib-coverage": "^3.2.0", "p-map": "^3.0.0", - "rimraf": "^6.1.3", - "uuid": "^8.3.2" + "rimraf": "^6.1.3" }, "dependencies": { "balanced-match": { @@ -9198,9 +8503,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -9416,39 +8721,12 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, "json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, - "keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "requires": { - "json-buffer": "3.0.1" - } - }, "klaw": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", @@ -9458,20 +8736,10 @@ "graceful-fs": "^4.1.9" } }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, "linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", "dev": true, "requires": { "uc.micro": "^2.0.0" @@ -9487,9 +8755,9 @@ } }, "lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true }, "lodash.camelcase": { @@ -9504,12 +8772,6 @@ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", "dev": true }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -9562,14 +8824,14 @@ "dev": true }, "markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", "dev": true, "requires": { "argparse": "^2.0.1", "entities": "^4.4.0", - "linkify-it": "^5.0.0", + "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -9674,9 +8936,9 @@ }, "dependencies": { "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true } } @@ -9694,15 +8956,6 @@ "mime-db": "^1.54.0" } }, - "minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -9734,9 +8987,9 @@ "dev": true }, "mocha": { - "version": "11.7.5", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", - "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "version": "11.7.6", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", + "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", "dev": true, "requires": { "browser-stdout": "^1.3.1", @@ -9763,9 +9016,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "requires": { "balanced-match": "^1.0.0" @@ -9804,15 +9057,9 @@ "dev": true }, "nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true }, "neo-async": { @@ -9878,9 +9125,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -10058,15 +9305,6 @@ "integrity": "sha512-6FuKFQ39cOID+BMZ3QaphcC8Y4cw6LXBLyIgPU+OhIYwviJamPAn+4mITapnSBQrejB+NNp+FMskhD8Cq+Ys3w==", "dev": true }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, "oniguruma-parser": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", @@ -10084,20 +9322,6 @@ "regex-recursion": "^6.0.2" } }, - "optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - } - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -10149,27 +9373,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -10207,9 +9416,9 @@ "dev": true }, "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true }, "pkg-dir": { @@ -10261,22 +9470,16 @@ } }, "postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "requires": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, "process-on-spawn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", @@ -10292,12 +9495,6 @@ "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "dev": true }, - "punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true - }, "punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -10379,12 +9576,6 @@ "lodash": "^4.17.21" } }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -10397,63 +9588,38 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, "rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", - "@types/estree": "1.0.8", + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "@types/estree": "1.0.9", "fsevents": "~2.3.2" } }, @@ -10530,22 +9696,21 @@ "dev": true }, "sinon": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.3.tgz", - "integrity": "sha512-0x8TQFr8EjADhSME01u1ZK31yv2+bd6Z5NrBCHVM+n4qL1wFqbxftmeyi3bwlr49FbbzRfrqSFOpyHCOh/YmYA==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-22.0.0.tgz", + "integrity": "sha512-sq/6DpdXOrLyfbKlXLg/Usc7xu8YXPeLkOFZRvA3bNUSA2lhbrZ06yuXbH1fkzBPCbz9O10+7hznzUsjaYNm0Q==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^15.1.1", - "@sinonjs/samsam": "^9.0.3", - "diff": "^8.0.3", - "supports-color": "^7.2.0" + "@sinonjs/fake-timers": "^15.4.0", + "@sinonjs/samsam": "^10.0.2", + "diff": "^9.0.0" }, "dependencies": { "diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true } } @@ -10600,9 +9765,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -10782,9 +9947,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -10828,20 +9993,14 @@ } } }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "requires": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" } }, "to-regex-range": { @@ -10859,33 +10018,18 @@ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", "dev": true }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, "type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "requires": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } @@ -10919,9 +10063,9 @@ "optional": true }, "underscore": { - "version": "1.13.7", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", - "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true }, "unist-util-is": { @@ -10982,21 +10126,6 @@ "picocolors": "^1.0.1" } }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - }, "vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -11018,9 +10147,9 @@ } }, "vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", "dev": true, "requires": { "esbuild": "^0.27.0", @@ -11033,43 +10162,43 @@ } }, "vitepress": { - "version": "2.0.0-alpha.16", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.16.tgz", - "integrity": "sha512-w1nwsefDVIsje7BZr2tsKxkZutDGjG0YoQ2yxO7+a9tvYVqfljYbwj5LMYkPy8Tb7YbPwa22HtIhk62jbrvuEQ==", + "version": "2.0.0-alpha.17", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.17.tgz", + "integrity": "sha512-Z3VPUpwk/bHYqt1uMVOOK1/4xFiWQov1GNc2FvMdz6kvje4JRXEOngVI9C+bi5jeedMSHiA4dwKkff1NCvbZ9Q==", "dev": true, "requires": { "@docsearch/css": "^4.5.3", "@docsearch/js": "^4.5.3", "@docsearch/sidepanel-js": "^4.5.3", - "@iconify-json/simple-icons": "^1.2.68", - "@shikijs/core": "^3.21.0", - "@shikijs/transformers": "^3.21.0", - "@shikijs/types": "^3.21.0", + "@iconify-json/simple-icons": "^1.2.69", + "@shikijs/core": "^3.22.0", + "@shikijs/transformers": "^3.22.0", + "@shikijs/types": "^3.22.0", "@types/markdown-it": "^14.1.2", - "@vitejs/plugin-vue": "^6.0.3", + "@vitejs/plugin-vue": "^6.0.4", "@vue/devtools-api": "^8.0.5", "@vue/shared": "^3.5.27", - "@vueuse/core": "^14.1.0", - "@vueuse/integrations": "^14.1.0", - "focus-trap": "^7.8.0", + "@vueuse/core": "^14.2.0", + "@vueuse/integrations": "^14.2.0", + "focus-trap": "^8.0.0", "mark.js": "8.11.1", "minisearch": "^7.2.0", - "shiki": "^3.21.0", + "shiki": "^3.22.0", "vite": "^7.3.1", "vue": "^3.5.27" } }, "vue": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", - "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz", + "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==", "dev": true, "requires": { - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-sfc": "3.5.27", - "@vue/runtime-dom": "3.5.27", - "@vue/server-renderer": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-sfc": "3.5.38", + "@vue/runtime-dom": "3.5.38", + "@vue/server-renderer": "3.5.38", + "@vue/shared": "3.5.38" } }, "walk-back": { @@ -11093,12 +10222,6 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true }, - "word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -11139,12 +10262,6 @@ "strip-ansi": "^6.0.0" } }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, "write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", diff --git a/package.json b/package.json index 21980ef7..3efd286d 100644 --- a/package.json +++ b/package.json @@ -28,16 +28,16 @@ "dependencies": { "@node-oauth/formats": "1.0.0", "basic-auth": "2.0.1", - "type-is": "2.0.1" + "type-is": "^2.1.0" }, "devDependencies": { + "@biomejs/biome": "2.5.0", "chai": "6.2.2", - "eslint": "8.57.1", "jsdoc-to-markdown": "^9.1.3", - "mocha": "11.7.5", + "mocha": "^11.7.6", "nyc": "18.0.0", - "sinon": "21.0.3", - "vitepress": "^2.0.0-alpha.15" + "sinon": "^22.0.0", + "vitepress": "^2.0.0-alpha.17" }, "license": "MIT", "engines": { @@ -49,8 +49,10 @@ "test-debug": "NODE_ENV=test ./node_modules/.bin/mocha --inspect --debug-brk 'test/**/*_test.js'", "test:watch": "NODE_ENV=test ./node_modules/.bin/mocha --watch 'test/**/*_test.js'", "test:coverage": "NODE_ENV=test nyc --reporter=html --reporter=lcov --reporter=text ./node_modules/.bin/mocha 'test/**/*_test.js'", - "lint": "npx eslint .", - "lint:fix": "npx eslint . --fix", + "lint": "npx @biomejs/biome lint --diagnostic-level=warn .", + "lint:fix": "npx @biomejs/biome lint --write .", + "format": "npx @biomejs/biome format --diagnostic-level=warn .", + "format:fix": "npx @biomejs/biome format --write .", "docs:dev": "vitepress dev docs", "docs:build": "npm run docs:setup && npm run docs:api && vitepress build docs", "docs:preview": "vitepress preview docs", From 08512376ca7a175add956a124a4792386be433c1 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Mon, 15 Jun 2026 10:31:43 +0200 Subject: [PATCH 02/11] fix(core): biome format fix --- .github/workflows/tests.yml | 8 +- biome.json | 69 +- lib/errors/access-denied-error.js | 30 +- lib/errors/insufficient-scope-error.js | 30 +- lib/errors/invalid-argument-error.js | 30 +- lib/errors/invalid-client-error.js | 30 +- lib/errors/invalid-grant-error.js | 30 +- lib/errors/invalid-request-error.js | 20 +- lib/errors/invalid-scope-error.js | 20 +- lib/errors/invalid-token-error.js | 20 +- lib/errors/oauth-error.js | 73 +- lib/errors/server-error.js | 30 +- lib/errors/unauthorized-client-error.js | 20 +- lib/errors/unauthorized-request-error.js | 20 +- lib/errors/unsupported-grant-type-error.js | 20 +- lib/errors/unsupported-response-type-error.js | 20 +- lib/grant-types/abstract-grant-type.js | 238 +- .../authorization-code-grant-type.js | 504 +-- .../client-credentials-grant-type.js | 124 +- lib/grant-types/password-grant-type.js | 166 +- lib/grant-types/refresh-token-grant-type.js | 298 +- lib/handlers/authenticate-handler.js | 564 +-- lib/handlers/authorize-handler.js | 843 ++-- lib/handlers/token-handler.js | 613 +-- lib/model.js | 431 +- lib/models/token-model.js | 124 +- lib/pkce/pkce.js | 61 +- lib/request.js | 108 +- lib/response-types/code-response-type.js | 66 +- lib/response-types/token-response-type.js | 18 +- lib/response.js | 97 +- lib/server.js | 441 ++- lib/token-types/bearer-token-type.js | 99 +- lib/token-types/mac-token-type.js | 18 +- lib/utils/crypto-util.js | 11 +- lib/utils/date-util.js | 6 +- lib/utils/scope-util.js | 32 +- lib/utils/string-util.js | 11 +- lib/utils/token-util.js | 24 +- test/assertions.js | 12 +- test/compliance/client-authentication_test.js | 202 +- .../client-credential-workflow_test.js | 213 +- test/compliance/password-grant-type_test.js | 339 +- test/compliance/pkce_test.js | 1278 +++--- .../refresh-token-grant-type_test.js | 268 +- test/helpers/db.js | 104 +- test/helpers/model.js | 192 +- test/helpers/request.js | 24 +- .../grant-types/abstract-grant-type_test.js | 513 ++- .../authorization-code-grant-type_test.js | 1560 +++++--- .../client-credentials-grant-type_test.js | 584 +-- .../grant-types/password-grant-type_test.js | 831 ++-- .../refresh-token-grant-type_test.js | 1321 ++++--- .../handlers/authenticate-handler_test.js | 1594 ++++---- .../handlers/authorize-handler_test.js | 3511 ++++++++++------- .../handlers/token-handler_test.js | 3140 +++++++++------ test/integration/request_test.js | 326 +- .../response-types/code-response-type_test.js | 92 +- test/integration/response_test.js | 98 +- test/integration/server_test.js | 409 +- .../token-types/bearer-token-type_test.js | 164 +- test/integration/utils/token-util_test.js | 18 +- test/unit/errors/oauth-error_test.js | 95 +- .../grant-types/abstract-grant-type_test.js | 84 +- .../authorization-code-grant-type_test.js | 319 +- .../client-credentials-grant-type_test.js | 116 +- .../grant-types/password-grant-type_test.js | 125 +- .../refresh-token-grant-type_test.js | 476 ++- .../handlers/authenticate-handler_test.js | 357 +- test/unit/handlers/authorize-handler_test.js | 473 ++- test/unit/handlers/token-handler_test.js | 62 +- test/unit/models/model_wrapper_test.js | 63 +- test/unit/models/token-model_test.js | 305 +- test/unit/pkce/pkce_test.js | 204 +- test/unit/request_test.js | 390 +- test/unit/response_test.js | 240 +- test/unit/server_test.js | 106 +- test/unit/utils/crypto-util_test.js | 34 +- test/unit/utils/date-util__test.js | 38 +- test/unit/utils/scope-util_test.js | 96 +- 80 files changed, 14373 insertions(+), 11370 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 90819cf4..8da764b9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ on: jobs: lint: - name: Javascript standard lint + name: Javascript Biome lint/format runs-on: ubuntu-latest steps: - name: Checkout repository @@ -28,8 +28,10 @@ jobs: with: node-version: 24 cache: npm - - run: npm clean-install - - run: npm run lint + - run: | + npm clean-install + npm run lint + npm run format unittest: name: unit tests diff --git a/biome.json b/biome.json index 56027c5e..a0af7e51 100644 --- a/biome.json +++ b/biome.json @@ -1,38 +1,35 @@ { - "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true - }, - "files": { - "ignoreUnknown": true, - "includes": [ - "lib/**/*.js", - "test/**/*.js" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - }, - "linter": { - "enabled": true, - "rules": { - "preset": "recommended" - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - }, - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": "on" - } - } - } + "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": ["lib/**/*.js", "test/**/*.js"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } } diff --git a/lib/errors/access-denied-error.js b/lib/errors/access-denied-error.js index e207f052..847add28 100644 --- a/lib/errors/access-denied-error.js +++ b/lib/errors/access-denied-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * @class @@ -12,20 +12,20 @@ const OAuthError = require('./oauth-error'); * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 */ class AccessDeniedError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 400, - name: 'access_denied', - ...properties - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 400, + name: "access_denied", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = AccessDeniedError; diff --git a/lib/errors/insufficient-scope-error.js b/lib/errors/insufficient-scope-error.js index e0fb876c..de96d878 100644 --- a/lib/errors/insufficient-scope-error.js +++ b/lib/errors/insufficient-scope-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * @class @@ -13,20 +13,20 @@ const OAuthError = require('./oauth-error'); */ class InsufficientScopeError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 403, - name: 'insufficient_scope', - ...properties - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 403, + name: "insufficient_scope", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = InsufficientScopeError; diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js index 46ba642d..1030232a 100644 --- a/lib/errors/invalid-argument-error.js +++ b/lib/errors/invalid-argument-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * @class @@ -12,20 +12,20 @@ const OAuthError = require('./oauth-error'); */ class InvalidArgumentError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 500, - name: 'invalid_argument', - ...properties - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 500, + name: "invalid_argument", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = InvalidArgumentError; diff --git a/lib/errors/invalid-client-error.js b/lib/errors/invalid-client-error.js index 33b15035..acf32a90 100644 --- a/lib/errors/invalid-client-error.js +++ b/lib/errors/invalid-client-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * @class @@ -14,20 +14,20 @@ const OAuthError = require('./oauth-error'); */ class InvalidClientError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 400, - name: 'invalid_client', - ...properties - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 400, + name: "invalid_client", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = InvalidClientError; diff --git a/lib/errors/invalid-grant-error.js b/lib/errors/invalid-grant-error.js index b91841f7..edc2dc64 100644 --- a/lib/errors/invalid-grant-error.js +++ b/lib/errors/invalid-grant-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * @class @@ -15,20 +15,20 @@ const OAuthError = require('./oauth-error'); */ class InvalidGrantError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 400, - name: 'invalid_grant', - ...properties - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 400, + name: "invalid_grant", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = InvalidGrantError; diff --git a/lib/errors/invalid-request-error.js b/lib/errors/invalid-request-error.js index 6692616e..3035b0c5 100644 --- a/lib/errors/invalid-request-error.js +++ b/lib/errors/invalid-request-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * Constructor. @@ -16,15 +16,15 @@ const OAuthError = require('./oauth-error'); */ class InvalidRequest extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: 'invalid_request', - ...properties - }; + constructor(message, properties) { + properties = { + code: 400, + name: "invalid_request", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js index 83098dcb..3211ab58 100644 --- a/lib/errors/invalid-scope-error.js +++ b/lib/errors/invalid-scope-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * Constructor. @@ -15,15 +15,15 @@ const OAuthError = require('./oauth-error'); */ class InvalidScopeError extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: 'invalid_scope', - ...properties - }; + constructor(message, properties) { + properties = { + code: 400, + name: "invalid_scope", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/invalid-token-error.js b/lib/errors/invalid-token-error.js index a3c0ff27..9f397fd9 100644 --- a/lib/errors/invalid-token-error.js +++ b/lib/errors/invalid-token-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * Constructor. @@ -15,15 +15,15 @@ const OAuthError = require('./oauth-error'); */ class InvalidTokenError extends OAuthError { - constructor(message, properties) { - properties = { - code: 401, - name: 'invalid_token', - ...properties - }; + constructor(message, properties) { + properties = { + code: 401, + name: "invalid_token", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/oauth-error.js b/lib/errors/oauth-error.js index 1291d913..bab5e501 100644 --- a/lib/errors/oauth-error.js +++ b/lib/errors/oauth-error.js @@ -1,49 +1,50 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const http = require('http'); +const http = require("http"); /** * Baseclass for OAuth related Error types. * @class */ class OAuthError extends Error { - /** - * @constructor - * @param messageOrError - * @param properties - */ - constructor(messageOrError, properties) { - super(messageOrError, properties); - - let message = messageOrError instanceof Error ? messageOrError.message : messageOrError; - const error = messageOrError instanceof Error ? messageOrError : null; - - if (properties == null || !Object.entries(properties).length) { - properties = {}; - } - - properties = { code: 500, ...properties }; - - if (error) { - properties.inner = error; - } - - if (!message || message.length === 0) { - message = http.STATUS_CODES[properties.code]; - } - - this.code = this.status = this.statusCode = properties.code; - this.message = message; - - for (const key in properties) { - if (key !== 'code') { - this[key] = properties[key]; - } - } - } + /** + * @constructor + * @param messageOrError + * @param properties + */ + constructor(messageOrError, properties) { + super(messageOrError, properties); + + let message = + messageOrError instanceof Error ? messageOrError.message : messageOrError; + const error = messageOrError instanceof Error ? messageOrError : null; + + if (properties == null || !Object.entries(properties).length) { + properties = {}; + } + + properties = { code: 500, ...properties }; + + if (error) { + properties.inner = error; + } + + if (!message || message.length === 0) { + message = http.STATUS_CODES[properties.code]; + } + + this.code = this.status = this.statusCode = properties.code; + this.message = message; + + for (const key in properties) { + if (key !== "code") { + this[key] = properties[key]; + } + } + } } /** diff --git a/lib/errors/server-error.js b/lib/errors/server-error.js index 04202097..f0df7603 100644 --- a/lib/errors/server-error.js +++ b/lib/errors/server-error.js @@ -1,11 +1,11 @@ -'use strict'; +"use strict"; /** * Module dependencies. * @private */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * "The authorization server encountered an unexpected condition that prevented it from fulfilling the request." @@ -14,20 +14,20 @@ const OAuthError = require('./oauth-error'); */ class ServerError extends OAuthError { - /** - * @constructor - * @param message - * @param properties - */ - constructor(message, properties) { - properties = { - code: 503, - name: 'server_error', - ...properties - }; + /** + * @constructor + * @param message + * @param properties + */ + constructor(message, properties) { + properties = { + code: 503, + name: "server_error", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/unauthorized-client-error.js b/lib/errors/unauthorized-client-error.js index 243c1acc..ab6ede29 100644 --- a/lib/errors/unauthorized-client-error.js +++ b/lib/errors/unauthorized-client-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * Constructor. @@ -15,15 +15,15 @@ const OAuthError = require('./oauth-error'); */ class UnauthorizedClientError extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: 'unauthorized_client', - ...properties - }; + constructor(message, properties) { + properties = { + code: 400, + name: "unauthorized_client", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/unauthorized-request-error.js b/lib/errors/unauthorized-request-error.js index a3f5ec63..a14af98a 100644 --- a/lib/errors/unauthorized-request-error.js +++ b/lib/errors/unauthorized-request-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * Constructor. @@ -18,15 +18,15 @@ const OAuthError = require('./oauth-error'); */ class UnauthorizedRequestError extends OAuthError { - constructor(message, properties) { - properties = { - code: 401, - name: 'unauthorized_request', - ...properties - }; + constructor(message, properties) { + properties = { + code: 401, + name: "unauthorized_request", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js index d15fd64c..5745d5d8 100644 --- a/lib/errors/unsupported-grant-type-error.js +++ b/lib/errors/unsupported-grant-type-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * Constructor. @@ -15,15 +15,15 @@ const OAuthError = require('./oauth-error'); */ class UnsupportedGrantTypeError extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: 'unsupported_grant_type', - ...properties - }; + constructor(message, properties) { + properties = { + code: 400, + name: "unsupported_grant_type", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/unsupported-response-type-error.js b/lib/errors/unsupported-response-type-error.js index c4b68fb5..fcedb039 100644 --- a/lib/errors/unsupported-response-type-error.js +++ b/lib/errors/unsupported-response-type-error.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const OAuthError = require('./oauth-error'); +const OAuthError = require("./oauth-error"); /** * Constructor. @@ -16,15 +16,15 @@ const OAuthError = require('./oauth-error'); */ class UnsupportedResponseTypeError extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: 'unsupported_response_type', - ...properties - }; + constructor(message, properties) { + properties = { + code: 400, + name: "unsupported_response_type", + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js index bfaa8d92..80e08bf2 100644 --- a/lib/grant-types/abstract-grant-type.js +++ b/lib/grant-types/abstract-grant-type.js @@ -1,130 +1,136 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const InvalidScopeError = require('../errors/invalid-scope-error'); -const tokenUtil = require('../utils/token-util'); -const { parseScope } = require('../utils/scope-util'); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const InvalidScopeError = require("../errors/invalid-scope-error"); +const tokenUtil = require("../utils/token-util"); +const { parseScope } = require("../utils/scope-util"); /** * @class * @classDesc# */ class AbstractGrantType { - /** - * @constructor - * @param options {object} - * @param options.accessTokenLifetime {number} access token lifetime in seconds - * @param options.model {Model} the model - * @param options.refreshTokenLifetime {number} - * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. - * @throws {InvalidArgumentError} if {options.accessTokenLifeTime} is missing - * @throws {InvalidArgumentError} if {options.model} is missing - */ - constructor (options) { - options = options || {}; - - if (!options.accessTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - this.accessTokenLifetime = options.accessTokenLifetime; - this.model = options.model; - this.refreshTokenLifetime = options.refreshTokenLifetime; - this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken; - } - - /** - * Generate access token. - * If the model implements `generateAccessToken` then - * this implementation will be used. - * Otherwise, falls back to an internal implementation from `TokenUtil.generateRandomToken`. - * - * @param client - * @param user - * @param scope - * @return {Promise} - */ - async generateAccessToken (client, user, scope) { - if (this.model.generateAccessToken) { - // We should not fall back to a random accessToken, if the model did not - // return a token, in order to prevent unintended token-issuing. - return this.model.generateAccessToken(client, user, scope); - } - - return tokenUtil.generateRandomToken(); - } - - /** - * Generate refresh token. - */ - async generateRefreshToken (client, user, scope) { - if (this.model.generateRefreshToken) { - // We should not fall back to a random refreshToken, if the model did not - // return a token, in order to prevent unintended token-issuing. - return this.model.generateRefreshToken(client, user, scope); - } - - return tokenUtil.generateRandomToken(); - } - - /** - * Get access token expiration date. - */ - getAccessTokenExpiresAt() { - return new Date(Date.now() + this.accessTokenLifetime * 1000); - } - - - - /** - * Get refresh token expiration date (now + refresh token lifetime) - * @returns {Date} - */ - getRefreshTokenExpiresAt () { - return new Date(Date.now() + this.refreshTokenLifetime * 1000); - } - - /** - * Get scope from the request body. - * @param request {Request} - * @returns {string|undefined} - */ - getScope (request) { - return parseScope(request.body.scope); - } - - /** - * Validate requested scope. - * Delegates validation to the Model's `validateScope` method, - * if the model implements this method. - * Otherwise, treats given scope as valid. - * @param user {object} - * @param client {ClientData} - * @param scope {string} - * @return {string} the validated scope - * @throws {InvalidScopeError} if the {Model#validateScope} method returned a falsy value - */ - async validateScope (user, client, scope) { - if (this.model.validateScope) { - const validatedScope = await this.model.validateScope(user, client, scope); - - if (!validatedScope) { - throw new InvalidScopeError('Invalid scope: Requested scope is invalid'); - } - - return validatedScope; - } else { - return scope; - } - } + /** + * @constructor + * @param options {object} + * @param options.accessTokenLifetime {number} access token lifetime in seconds + * @param options.model {Model} the model + * @param options.refreshTokenLifetime {number} + * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. + * @throws {InvalidArgumentError} if {options.accessTokenLifeTime} is missing + * @throws {InvalidArgumentError} if {options.model} is missing + */ + constructor(options) { + options = options || {}; + + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + "Missing parameter: `accessTokenLifetime`", + ); + } + + if (!options.model) { + throw new InvalidArgumentError("Missing parameter: `model`"); + } + + this.accessTokenLifetime = options.accessTokenLifetime; + this.model = options.model; + this.refreshTokenLifetime = options.refreshTokenLifetime; + this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken; + } + + /** + * Generate access token. + * If the model implements `generateAccessToken` then + * this implementation will be used. + * Otherwise, falls back to an internal implementation from `TokenUtil.generateRandomToken`. + * + * @param client + * @param user + * @param scope + * @return {Promise} + */ + async generateAccessToken(client, user, scope) { + if (this.model.generateAccessToken) { + // We should not fall back to a random accessToken, if the model did not + // return a token, in order to prevent unintended token-issuing. + return this.model.generateAccessToken(client, user, scope); + } + + return tokenUtil.generateRandomToken(); + } + + /** + * Generate refresh token. + */ + async generateRefreshToken(client, user, scope) { + if (this.model.generateRefreshToken) { + // We should not fall back to a random refreshToken, if the model did not + // return a token, in order to prevent unintended token-issuing. + return this.model.generateRefreshToken(client, user, scope); + } + + return tokenUtil.generateRandomToken(); + } + + /** + * Get access token expiration date. + */ + getAccessTokenExpiresAt() { + return new Date(Date.now() + this.accessTokenLifetime * 1000); + } + + /** + * Get refresh token expiration date (now + refresh token lifetime) + * @returns {Date} + */ + getRefreshTokenExpiresAt() { + return new Date(Date.now() + this.refreshTokenLifetime * 1000); + } + + /** + * Get scope from the request body. + * @param request {Request} + * @returns {string|undefined} + */ + getScope(request) { + return parseScope(request.body.scope); + } + + /** + * Validate requested scope. + * Delegates validation to the Model's `validateScope` method, + * if the model implements this method. + * Otherwise, treats given scope as valid. + * @param user {object} + * @param client {ClientData} + * @param scope {string} + * @return {string} the validated scope + * @throws {InvalidScopeError} if the {Model#validateScope} method returned a falsy value + */ + async validateScope(user, client, scope) { + if (this.model.validateScope) { + const validatedScope = await this.model.validateScope( + user, + client, + scope, + ); + + if (!validatedScope) { + throw new InvalidScopeError( + "Invalid scope: Requested scope is invalid", + ); + } + + return validatedScope; + } else { + return scope; + } + } } module.exports = AbstractGrantType; diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 56df29ec..3ef8366d 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -1,196 +1,233 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const crypto = require('crypto'); -const AbstractGrantType = require('./abstract-grant-type'); -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const InvalidGrantError = require('../errors/invalid-grant-error'); -const InvalidRequestError = require('../errors/invalid-request-error'); -const ServerError = require('../errors/server-error'); -const isFormat = require('@node-oauth/formats'); -const pkce = require('../pkce/pkce'); +const crypto = require("crypto"); +const AbstractGrantType = require("./abstract-grant-type"); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const InvalidGrantError = require("../errors/invalid-grant-error"); +const InvalidRequestError = require("../errors/invalid-request-error"); +const ServerError = require("../errors/server-error"); +const isFormat = require("@node-oauth/formats"); +const pkce = require("../pkce/pkce"); /** * @class * @classDesc */ class AuthorizationCodeGrantType extends AbstractGrantType { - /** - * @constructor - * @param options - */ - constructor(options = {}) { - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getAuthorizationCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getAuthorizationCode()`'); - } - - if (!options.model.revokeAuthorizationCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `revokeAuthorizationCode()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - super(options); - - // xxx: plain PKCE is only allowed if explicitly enabled - this.enablePlainPKCE = options.enablePlainPKCE === true; - } - - /** + /** + * @constructor + * @param options + */ + constructor(options = {}) { + if (!options.model) { + throw new InvalidArgumentError("Missing parameter: `model`"); + } + + if (!options.model.getAuthorizationCode) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `getAuthorizationCode()`", + ); + } + + if (!options.model.revokeAuthorizationCode) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `revokeAuthorizationCode()`", + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `saveToken()`", + ); + } + + super(options); + + // xxx: plain PKCE is only allowed if explicitly enabled + this.enablePlainPKCE = options.enablePlainPKCE === true; + } + + /** * Handle authorization code grant. * - * @param request {Request} - * @param client {ClientData} + * @param request {Request} + * @param client {ClientData} * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 */ - async handle(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } - - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } - - const code = await this.getAuthorizationCode(request, client); - await this.revokeAuthorizationCode(code); - // xxx: PKCE verification is done after revoking the code, - // so that a failed verification attempt consumes the code and prevents - // online brute-force guessing. - await this.verifyPKCE(request, code); - await this.validateRedirectUri(request, code); - - return this.saveToken(code.user, client, code.authorizationCode, code.scope); - } - - /** - * Get the authorization code. - * @param request {Request} - * @param client {ClientData} - * @return {Promise<{user}>} - */ - - async getAuthorizationCode(request, client) { - if (!request.body.code) { - throw new InvalidRequestError('Missing parameter: `code`'); - } - - if (!isFormat.vschar(request.body.code)) { - throw new InvalidRequestError('Invalid parameter: `code`'); - } - - const code = await this.model.getAuthorizationCode(request.body.code); - - if (!code) { - throw new InvalidGrantError('Invalid grant: authorization code is invalid'); - } - - if (!code.client) { - throw new ServerError('Server error: `getAuthorizationCode()` did not return a `client` object'); - } - - if (!code.user) { - throw new ServerError('Server error: `getAuthorizationCode()` did not return a `user` object'); - } - - if (code.client.id !== client.id) { - throw new InvalidGrantError('Invalid grant: authorization code is invalid'); - } - - if (!(code.expiresAt instanceof Date)) { - throw new ServerError('Server error: `expiresAt` must be a Date instance'); - } - - if (code.expiresAt < new Date()) { - throw new InvalidGrantError('Invalid grant: authorization code has expired'); - } - - if (code.redirectUri && !isFormat.uri(code.redirectUri)) { - throw new InvalidGrantError('Invalid grant: `redirect_uri` is not a valid URI'); - } - - return code; - } - - /** - * Verify PKCE code_verifier against the stored code_challenge. - * - * This is called from handle() AFTER the authorization code has been - * revoked, so that a failed verification attempt consumes the code - * and prevents online brute-force guessing. - * - * @param request {Request} - * @param code {AuthorizationCodeData} - * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6 - */ - - verifyPKCE(request, code) { - if (code.codeChallenge) { - const method = this.getCodeChallengeMethod(code.codeChallengeMethod); - - if (!this.enablePlainPKCE && method === 'plain') { - throw new InvalidRequestError('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); - } - - if (!request.body.code_verifier) { - throw new InvalidGrantError('Missing parameter: `code_verifier`'); - } - - if (!pkce.codeChallengeMatchesABNF(request.body.code_verifier)) { - throw new InvalidRequestError('Invalid parameter: `code_verifier`'); - } - - const hash = pkce.getHashForCodeChallenge({ - verifier: request.body.code_verifier, - method - }); - - if (!hash) { - throw new ServerError('Server error: no valid hash algorithm available to verify `code_verifier`'); - } - - // xxx: Use timingSafeEqual to prevent against timing attacks when comparing - // the hash of the code_verifier to the stored code_challenge. - if (!this.hashesAreEqual(hash, code.codeChallenge)) { - throw new InvalidGrantError('Invalid grant: code verifier is invalid'); - } - } - else { - if (request.body.code_verifier) { - // No code challenge but code_verifier was passed in. - throw new InvalidGrantError('Invalid grant: code verifier is invalid'); - } - } - } - - hashesAreEqual(trusted, untrusted) { - const trustedBuf = Buffer.isBuffer(trusted) ? trusted : Buffer.from(trusted); - const untrustedBuf = Buffer.isBuffer(untrusted) ? untrusted : Buffer.from(untrusted); - const equalLength = trustedBuf.byteLength === untrustedBuf.byteLength; - // if the buffers are the same length, compare them, - // otherwise only compare with the trusted buffer but return false anyway - return crypto.timingSafeEqual(trustedBuf, equalLength ? untrustedBuf : trustedBuf) && equalLength; - } - - getCodeChallengeMethod(method) { - if (method) { - return method; - } - // Per RFC 7636 §4.6, the default code challenge method is "plain". - // However, plain PKCE is not secure, so we only allow it if explicitly enabled. - return this.enablePlainPKCE ? 'plain' : 'S256'; - } - - /** + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError("Missing parameter: `request`"); + } + + if (!client) { + throw new InvalidArgumentError("Missing parameter: `client`"); + } + + const code = await this.getAuthorizationCode(request, client); + await this.revokeAuthorizationCode(code); + // xxx: PKCE verification is done after revoking the code, + // so that a failed verification attempt consumes the code and prevents + // online brute-force guessing. + await this.verifyPKCE(request, code); + await this.validateRedirectUri(request, code); + + return this.saveToken( + code.user, + client, + code.authorizationCode, + code.scope, + ); + } + + /** + * Get the authorization code. + * @param request {Request} + * @param client {ClientData} + * @return {Promise<{user}>} + */ + + async getAuthorizationCode(request, client) { + if (!request.body.code) { + throw new InvalidRequestError("Missing parameter: `code`"); + } + + if (!isFormat.vschar(request.body.code)) { + throw new InvalidRequestError("Invalid parameter: `code`"); + } + + const code = await this.model.getAuthorizationCode(request.body.code); + + if (!code) { + throw new InvalidGrantError( + "Invalid grant: authorization code is invalid", + ); + } + + if (!code.client) { + throw new ServerError( + "Server error: `getAuthorizationCode()` did not return a `client` object", + ); + } + + if (!code.user) { + throw new ServerError( + "Server error: `getAuthorizationCode()` did not return a `user` object", + ); + } + + if (code.client.id !== client.id) { + throw new InvalidGrantError( + "Invalid grant: authorization code is invalid", + ); + } + + if (!(code.expiresAt instanceof Date)) { + throw new ServerError( + "Server error: `expiresAt` must be a Date instance", + ); + } + + if (code.expiresAt < new Date()) { + throw new InvalidGrantError( + "Invalid grant: authorization code has expired", + ); + } + + if (code.redirectUri && !isFormat.uri(code.redirectUri)) { + throw new InvalidGrantError( + "Invalid grant: `redirect_uri` is not a valid URI", + ); + } + + return code; + } + + /** + * Verify PKCE code_verifier against the stored code_challenge. + * + * This is called from handle() AFTER the authorization code has been + * revoked, so that a failed verification attempt consumes the code + * and prevents online brute-force guessing. + * + * @param request {Request} + * @param code {AuthorizationCodeData} + * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6 + */ + + verifyPKCE(request, code) { + if (code.codeChallenge) { + const method = this.getCodeChallengeMethod(code.codeChallengeMethod); + + if (!this.enablePlainPKCE && method === "plain") { + throw new InvalidRequestError( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } + + if (!request.body.code_verifier) { + throw new InvalidGrantError("Missing parameter: `code_verifier`"); + } + + if (!pkce.codeChallengeMatchesABNF(request.body.code_verifier)) { + throw new InvalidRequestError("Invalid parameter: `code_verifier`"); + } + + const hash = pkce.getHashForCodeChallenge({ + verifier: request.body.code_verifier, + method, + }); + + if (!hash) { + throw new ServerError( + "Server error: no valid hash algorithm available to verify `code_verifier`", + ); + } + + // xxx: Use timingSafeEqual to prevent against timing attacks when comparing + // the hash of the code_verifier to the stored code_challenge. + if (!this.hashesAreEqual(hash, code.codeChallenge)) { + throw new InvalidGrantError("Invalid grant: code verifier is invalid"); + } + } else { + if (request.body.code_verifier) { + // No code challenge but code_verifier was passed in. + throw new InvalidGrantError("Invalid grant: code verifier is invalid"); + } + } + } + + hashesAreEqual(trusted, untrusted) { + const trustedBuf = Buffer.isBuffer(trusted) + ? trusted + : Buffer.from(trusted); + const untrustedBuf = Buffer.isBuffer(untrusted) + ? untrusted + : Buffer.from(untrusted); + const equalLength = trustedBuf.byteLength === untrustedBuf.byteLength; + // if the buffers are the same length, compare them, + // otherwise only compare with the trusted buffer but return false anyway + return ( + crypto.timingSafeEqual( + trustedBuf, + equalLength ? untrustedBuf : trustedBuf, + ) && equalLength + ); + } + + getCodeChallengeMethod(method) { + if (method) { + return method; + } + // Per RFC 7636 §4.6, the default code challenge method is "plain". + // However, plain PKCE is not secure, so we only allow it if explicitly enabled. + return this.enablePlainPKCE ? "plain" : "S256"; + } + + /** * Validate the redirect URI. * * "The authorization server MUST ensure that the redirect_uri parameter is @@ -198,27 +235,31 @@ class AuthorizationCodeGrantType extends AbstractGrantType { * authorization request as described in Section 4.1.1, and if included * ensure that their values are identical." * @param request {Request} - * @param code {AuthorizationCodeData} + * @param code {AuthorizationCodeData} * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 */ - validateRedirectUri(request, code) { - if (!code.redirectUri) { - return; - } + validateRedirectUri(request, code) { + if (!code.redirectUri) { + return; + } - const redirectUri = request.body.redirect_uri || request.query.redirect_uri; + const redirectUri = request.body.redirect_uri || request.query.redirect_uri; - if (!isFormat.uri(redirectUri)) { - throw new InvalidRequestError('Invalid request: `redirect_uri` is not a valid URI'); - } + if (!isFormat.uri(redirectUri)) { + throw new InvalidRequestError( + "Invalid request: `redirect_uri` is not a valid URI", + ); + } - if (redirectUri !== code.redirectUri) { - throw new InvalidRequestError('Invalid request: `redirect_uri` is invalid'); - } - } + if (redirectUri !== code.redirectUri) { + throw new InvalidRequestError( + "Invalid request: `redirect_uri` is invalid", + ); + } + } - /** + /** * Revoke the authorization code. * * "The authorization code MUST expire shortly after it is issued to mitigate @@ -228,45 +269,58 @@ class AuthorizationCodeGrantType extends AbstractGrantType { * @see https://tools.ietf.org/html/rfc6749#section-4.1.2 */ - async revokeAuthorizationCode(code) { - const status = await this.model.revokeAuthorizationCode(code); + async revokeAuthorizationCode(code) { + const status = await this.model.revokeAuthorizationCode(code); - if (!status) { - throw new InvalidGrantError('Invalid grant: authorization code is invalid'); - } + if (!status) { + throw new InvalidGrantError( + "Invalid grant: authorization code is invalid", + ); + } - return code; - } + return code; + } - - /** + /** * Save token. - * - * @param user {object} - * @param client {ClientData} - * @param authorizationCode {string} - * @param requestedScope {string} - * + * + * @param user {object} + * @param client {ClientData} + * @param authorizationCode {string} + * @param requestedScope {string} + * */ - async saveToken(user, client, authorizationCode, requestedScope) { - const validatedScope = await this.validateScope(user, client, requestedScope); - const accessToken = await this.generateAccessToken(client, user, validatedScope); - const refreshToken = await this.generateRefreshToken(client, user, validatedScope); - const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); - const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); - - const token = { - accessToken, - authorizationCode, - accessTokenExpiresAt, - refreshToken, - refreshTokenExpiresAt, - scope: validatedScope, - }; - - return this.model.saveToken(token, client, user); - } + async saveToken(user, client, authorizationCode, requestedScope) { + const validatedScope = await this.validateScope( + user, + client, + requestedScope, + ); + const accessToken = await this.generateAccessToken( + client, + user, + validatedScope, + ); + const refreshToken = await this.generateRefreshToken( + client, + user, + validatedScope, + ); + const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); + + const token = { + accessToken, + authorizationCode, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope: validatedScope, + }; + + return this.model.saveToken(token, client, user); + } } /** diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index 5e6e74f6..b06e0a12 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -1,85 +1,103 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const AbstractGrantType = require('./abstract-grant-type'); -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const InvalidGrantError = require('../errors/invalid-grant-error'); +const AbstractGrantType = require("./abstract-grant-type"); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const InvalidGrantError = require("../errors/invalid-grant-error"); /** * Constructor. */ class ClientCredentialsGrantType extends AbstractGrantType { - constructor(options = {}) { - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getUserFromClient) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getUserFromClient()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - super(options); - } - - /** + constructor(options = {}) { + if (!options.model) { + throw new InvalidArgumentError("Missing parameter: `model`"); + } + + if (!options.model.getUserFromClient) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `getUserFromClient()`", + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `saveToken()`", + ); + } + + super(options); + } + + /** * Handle client credentials grant. * * @see https://tools.ietf.org/html/rfc6749#section-4.4.2 */ - async handle(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError("Missing parameter: `request`"); + } - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } + if (!client) { + throw new InvalidArgumentError("Missing parameter: `client`"); + } - const scope = this.getScope(request); - const user = await this.getUserFromClient(client); + const scope = this.getScope(request); + const user = await this.getUserFromClient(client); - return this.saveToken(user, client, scope); - } + return this.saveToken(user, client, scope); + } - /** + /** * Retrieve the user using client credentials. */ - async getUserFromClient(client) { - const user = await this.model.getUserFromClient(client); + async getUserFromClient(client) { + const user = await this.model.getUserFromClient(client); - if (!user) { - throw new InvalidGrantError('Invalid grant: user credentials are invalid'); - } + if (!user) { + throw new InvalidGrantError( + "Invalid grant: user credentials are invalid", + ); + } - return user; - } + return user; + } - /** + /** * Save token. */ - async saveToken(user, client, requestedScope) { - const validatedScope = await this.validateScope(user, client, requestedScope); - const accessToken = await this.generateAccessToken(client, user, validatedScope); - const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(client, user, validatedScope); - const token = { - accessToken, - accessTokenExpiresAt, - scope: validatedScope, - }; - - return this.model.saveToken(token, client, user); - } + async saveToken(user, client, requestedScope) { + const validatedScope = await this.validateScope( + user, + client, + requestedScope, + ); + const accessToken = await this.generateAccessToken( + client, + user, + validatedScope, + ); + const accessTokenExpiresAt = await this.getAccessTokenExpiresAt( + client, + user, + validatedScope, + ); + const token = { + accessToken, + accessTokenExpiresAt, + scope: validatedScope, + }; + + return this.model.saveToken(token, client, user); + } } /** diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index bcb32c3a..652afd9f 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -1,14 +1,14 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const AbstractGrantType = require('./abstract-grant-type'); -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const InvalidGrantError = require('../errors/invalid-grant-error'); -const InvalidRequestError = require('../errors/invalid-request-error'); -const isFormat = require('@node-oauth/formats'); +const AbstractGrantType = require("./abstract-grant-type"); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const InvalidGrantError = require("../errors/invalid-grant-error"); +const InvalidRequestError = require("../errors/invalid-request-error"); +const isFormat = require("@node-oauth/formats"); /** * Constructor. @@ -16,94 +16,116 @@ const isFormat = require('@node-oauth/formats'); */ class PasswordGrantType extends AbstractGrantType { - constructor(options = {}) { - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getUser) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getUser()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - super(options); - } - - /** + constructor(options = {}) { + if (!options.model) { + throw new InvalidArgumentError("Missing parameter: `model`"); + } + + if (!options.model.getUser) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `getUser()`", + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `saveToken()`", + ); + } + + super(options); + } + + /** * Retrieve the user from the model using a username/password combination. * * @see https://tools.ietf.org/html/rfc6749#section-4.3.2 */ - async handle(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError("Missing parameter: `request`"); + } - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } + if (!client) { + throw new InvalidArgumentError("Missing parameter: `client`"); + } - const scope = this.getScope(request); - const user = await this.getUser(request, client); + const scope = this.getScope(request); + const user = await this.getUser(request, client); - return this.saveToken(user, client, scope); - } + return this.saveToken(user, client, scope); + } - /** + /** * Get user using a username/password combination. */ - async getUser(request, client) { - if (!request.body.username) { - throw new InvalidRequestError('Missing parameter: `username`'); - } + async getUser(request, client) { + if (!request.body.username) { + throw new InvalidRequestError("Missing parameter: `username`"); + } - if (!request.body.password) { - throw new InvalidRequestError('Missing parameter: `password`'); - } + if (!request.body.password) { + throw new InvalidRequestError("Missing parameter: `password`"); + } - if (!isFormat.uchar(request.body.username)) { - throw new InvalidRequestError('Invalid parameter: `username`'); - } + if (!isFormat.uchar(request.body.username)) { + throw new InvalidRequestError("Invalid parameter: `username`"); + } - if (!isFormat.uchar(request.body.password)) { - throw new InvalidRequestError('Invalid parameter: `password`'); - } + if (!isFormat.uchar(request.body.password)) { + throw new InvalidRequestError("Invalid parameter: `password`"); + } - const user = await this.model.getUser(request.body.username, request.body.password, client); + const user = await this.model.getUser( + request.body.username, + request.body.password, + client, + ); - if (!user) { - throw new InvalidGrantError('Invalid grant: user credentials are invalid'); - } + if (!user) { + throw new InvalidGrantError( + "Invalid grant: user credentials are invalid", + ); + } - return user; - } + return user; + } - /** + /** * Save token. */ - async saveToken(user, client, requestedScope) { - const validatedScope = await this.validateScope(user, client, requestedScope); - const accessToken = await this.generateAccessToken(client, user, validatedScope); - const refreshToken = await this.generateRefreshToken(client, user, validatedScope); - const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); - const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); - - const token = { - accessToken, - accessTokenExpiresAt, - refreshToken, - refreshTokenExpiresAt, - scope: validatedScope, - }; - - return this.model.saveToken(token, client, user); - } + async saveToken(user, client, requestedScope) { + const validatedScope = await this.validateScope( + user, + client, + requestedScope, + ); + const accessToken = await this.generateAccessToken( + client, + user, + validatedScope, + ); + const refreshToken = await this.generateRefreshToken( + client, + user, + validatedScope, + ); + const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); + + const token = { + accessToken, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope: validatedScope, + }; + + return this.model.saveToken(token, client, user); + } } /** diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index 1f62f7ec..451074b4 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -1,177 +1,199 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const AbstractGrantType = require('./abstract-grant-type'); -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const InvalidGrantError = require('../errors/invalid-grant-error'); -const InvalidRequestError = require('../errors/invalid-request-error'); -const ServerError = require('../errors/server-error'); -const isFormat = require('@node-oauth/formats'); -const InvalidScopeError = require('../errors/invalid-scope-error'); +const AbstractGrantType = require("./abstract-grant-type"); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const InvalidGrantError = require("../errors/invalid-grant-error"); +const InvalidRequestError = require("../errors/invalid-request-error"); +const ServerError = require("../errors/server-error"); +const isFormat = require("@node-oauth/formats"); +const InvalidScopeError = require("../errors/invalid-scope-error"); /** * Constructor. */ class RefreshTokenGrantType extends AbstractGrantType { - constructor(options = {}) { - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getRefreshToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getRefreshToken()`'); - } - - if (!options.model.revokeToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `revokeToken()`'); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); - } - - super(options); - } - - /** + constructor(options = {}) { + if (!options.model) { + throw new InvalidArgumentError("Missing parameter: `model`"); + } + + if (!options.model.getRefreshToken) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `getRefreshToken()`", + ); + } + + if (!options.model.revokeToken) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `revokeToken()`", + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `saveToken()`", + ); + } + + super(options); + } + + /** * Handle refresh token grant. * * @see https://tools.ietf.org/html/rfc6749#section-6 */ - async handle(request, client) { - if (!request) { - throw new InvalidArgumentError('Missing parameter: `request`'); - } + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError("Missing parameter: `request`"); + } - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } + if (!client) { + throw new InvalidArgumentError("Missing parameter: `client`"); + } - let token; - token = await this.getRefreshToken(request, client); - token = await this.revokeToken(token); + let token; + token = await this.getRefreshToken(request, client); + token = await this.revokeToken(token); - const scope = this.getScope(request, token); + const scope = this.getScope(request, token); - return this.saveToken(token.user, client, scope); - } + return this.saveToken(token.user, client, scope); + } - /** + /** * Get refresh token. */ - async getRefreshToken(request, client) { - if (!request.body.refresh_token) { - throw new InvalidRequestError('Missing parameter: `refresh_token`'); - } - - if (!isFormat.vschar(request.body.refresh_token)) { - throw new InvalidRequestError('Invalid parameter: `refresh_token`'); - } - - const token = await this.model.getRefreshToken(request.body.refresh_token); - - if (!token) { - throw new InvalidGrantError('Invalid grant: refresh token is invalid'); - } - - if (!token.client) { - throw new ServerError('Server error: `getRefreshToken()` did not return a `client` object'); - } - - if (!token.user) { - throw new ServerError('Server error: `getRefreshToken()` did not return a `user` object'); - } - - if (token.client.id !== client.id) { - throw new InvalidGrantError('Invalid grant: refresh token was issued to another client'); - } - - if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) { - throw new ServerError('Server error: `refreshTokenExpiresAt` must be a Date instance'); - } - - if (token.refreshTokenExpiresAt && token.refreshTokenExpiresAt < new Date()) { - throw new InvalidGrantError('Invalid grant: refresh token has expired'); - } - - return token; - } - - /** + async getRefreshToken(request, client) { + if (!request.body.refresh_token) { + throw new InvalidRequestError("Missing parameter: `refresh_token`"); + } + + if (!isFormat.vschar(request.body.refresh_token)) { + throw new InvalidRequestError("Invalid parameter: `refresh_token`"); + } + + const token = await this.model.getRefreshToken(request.body.refresh_token); + + if (!token) { + throw new InvalidGrantError("Invalid grant: refresh token is invalid"); + } + + if (!token.client) { + throw new ServerError( + "Server error: `getRefreshToken()` did not return a `client` object", + ); + } + + if (!token.user) { + throw new ServerError( + "Server error: `getRefreshToken()` did not return a `user` object", + ); + } + + if (token.client.id !== client.id) { + throw new InvalidGrantError( + "Invalid grant: refresh token was issued to another client", + ); + } + + if ( + token.refreshTokenExpiresAt && + !(token.refreshTokenExpiresAt instanceof Date) + ) { + throw new ServerError( + "Server error: `refreshTokenExpiresAt` must be a Date instance", + ); + } + + if ( + token.refreshTokenExpiresAt && + token.refreshTokenExpiresAt < new Date() + ) { + throw new InvalidGrantError("Invalid grant: refresh token has expired"); + } + + return token; + } + + /** * Revoke the refresh token. * * @see https://tools.ietf.org/html/rfc6749#section-6 */ - async revokeToken(token) { - if (this.alwaysIssueNewRefreshToken === false) { - return token; - } + async revokeToken(token) { + if (this.alwaysIssueNewRefreshToken === false) { + return token; + } - const status = await this.model.revokeToken(token); + const status = await this.model.revokeToken(token); - if (!status) { - throw new InvalidGrantError('Invalid grant: refresh token is invalid or could not be revoked'); - } + if (!status) { + throw new InvalidGrantError( + "Invalid grant: refresh token is invalid or could not be revoked", + ); + } - return token; - } + return token; + } - /** + /** * Save token. */ - async saveToken(user, client, scope) { - const accessToken = await this.generateAccessToken(client, user, scope); - const refreshToken = await this.generateRefreshToken(client, user, scope); - const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); - const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); - const token = { - accessToken, - accessTokenExpiresAt, - scope, - }; - - if (this.alwaysIssueNewRefreshToken !== false) { - token.refreshToken = refreshToken; - token.refreshTokenExpiresAt = refreshTokenExpiresAt; - } - - return this.model.saveToken(token, client, user); - } - - getScope (request, token) { - const requestedScope = super.getScope(request); - const originalScope = token.scope; - - if (!originalScope && !requestedScope) { - return; - } - - if (!originalScope && requestedScope) { - throw new InvalidScopeError('Invalid scope: Unable to add extra scopes'); - } - - if (!requestedScope) { - return originalScope; - } - - const valid = requestedScope.every(scope => { - return originalScope.includes(scope); - }); - - if (!valid) { - throw new InvalidScopeError('Invalid scope: Unable to add extra scopes'); - } - - return requestedScope; - } + async saveToken(user, client, scope) { + const accessToken = await this.generateAccessToken(client, user, scope); + const refreshToken = await this.generateRefreshToken(client, user, scope); + const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); + const token = { + accessToken, + accessTokenExpiresAt, + scope, + }; + + if (this.alwaysIssueNewRefreshToken !== false) { + token.refreshToken = refreshToken; + token.refreshTokenExpiresAt = refreshTokenExpiresAt; + } + + return this.model.saveToken(token, client, user); + } + + getScope(request, token) { + const requestedScope = super.getScope(request); + const originalScope = token.scope; + + if (!originalScope && !requestedScope) { + return; + } + + if (!originalScope && requestedScope) { + throw new InvalidScopeError("Invalid scope: Unable to add extra scopes"); + } + + if (!requestedScope) { + return originalScope; + } + + const valid = requestedScope.every((scope) => { + return originalScope.includes(scope); + }); + + if (!valid) { + throw new InvalidScopeError("Invalid scope: Unable to add extra scopes"); + } + + return requestedScope; + } } /** diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 54674334..979c7912 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -1,275 +1,317 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const InvalidRequestError = require('../errors/invalid-request-error'); -const InsufficientScopeError = require('../errors/insufficient-scope-error'); -const InvalidTokenError = require('../errors/invalid-token-error'); -const OAuthError = require('../errors/oauth-error'); -const Request = require('../request'); -const Response = require('../response'); -const ServerError = require('../errors/server-error'); -const UnauthorizedRequestError = require('../errors/unauthorized-request-error'); -const { parseScope } = require('../utils/scope-util'); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const InvalidRequestError = require("../errors/invalid-request-error"); +const InsufficientScopeError = require("../errors/insufficient-scope-error"); +const InvalidTokenError = require("../errors/invalid-token-error"); +const OAuthError = require("../errors/oauth-error"); +const Request = require("../request"); +const Response = require("../response"); +const ServerError = require("../errors/server-error"); +const UnauthorizedRequestError = require("../errors/unauthorized-request-error"); +const { parseScope } = require("../utils/scope-util"); /** * @class * @classDesc */ class AuthenticateHandler { - /** - * @constructor - * @param options {object} Server options. - * @param options.model {Model} The Model; this is always required. - * @param options.scope {string[]|undefined} The scope(s) to authenticate. - * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. - * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. - * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. - * @throws {InvalidArgumentError} if {model} is missing or does not implement `getAccessToken` - */ - constructor (options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getAccessToken) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getAccessToken()`'); - } - - if (options.scope && undefined === options.addAcceptedScopesHeader) { - throw new InvalidArgumentError('Missing parameter: `addAcceptedScopesHeader`'); - } - - if (options.scope && undefined === options.addAuthorizedScopesHeader) { - throw new InvalidArgumentError('Missing parameter: `addAuthorizedScopesHeader`'); - } - - if (options.scope && !options.model.verifyScope) { - throw new InvalidArgumentError('Invalid argument: model does not implement `verifyScope()`'); - } - - this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; - this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; - this.allowBearerTokensInQueryString = options.allowBearerTokensInQueryString; - this.model = options.model; - this.scope = Array.isArray(options.scope) ? options.scope : parseScope(options.scope); - } - - /** - * Handles the authentication - * @param request {Request} - * @param response {Response} - * @return {Promise<*>} - */ - async handle (request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); - } - - try { - const requestToken = await this.getTokenFromRequest(request); - - let accessToken; - accessToken = await this.getAccessToken(requestToken); - accessToken = await this.validateAccessToken(accessToken); - - if (this.scope) { - await this.verifyScope(accessToken); - } - - this.updateResponse(response, accessToken); - - return accessToken; - } catch (e) { - // Include the "WWW-Authenticate" response header field if the client - // lacks any authentication information. - // - // @see https://tools.ietf.org/html/rfc6750#section-3.1 - if (e instanceof UnauthorizedRequestError) { - response.set('WWW-Authenticate', 'Bearer realm="Service"'); - } else if (e instanceof InvalidRequestError) { - response.set('WWW-Authenticate', 'Bearer realm="Service",error="invalid_request"'); - } else if (e instanceof InvalidTokenError) { - response.set('WWW-Authenticate', 'Bearer realm="Service",error="invalid_token"'); - } else if (e instanceof InsufficientScopeError) { - response.set('WWW-Authenticate', 'Bearer realm="Service",error="insufficient_scope"'); - } - - if (!(e instanceof OAuthError)) { - throw new ServerError(e); - } - - throw e; - } - } - - /** - * Get the token from the header or body, depending on the request. - * - * "Clients MUST NOT use more than one method to transmit the token in each request." - * - * @param {Request} - * @see {https://tools.ietf.org/html/rfc6750#section-2} - */ - getTokenFromRequest (request) { - const headerToken = request.get('Authorization'); - const queryToken = request.query.access_token; - const bodyToken = request.body.access_token; - - if (!!headerToken + !!queryToken + !!bodyToken > 1) { - throw new InvalidRequestError('Invalid request: only one authentication method is allowed'); - } - - if (headerToken) { - return this.getTokenFromRequestHeader(request); - } - - if (queryToken) { - return this.getTokenFromRequestQuery(request); - } - - if (bodyToken) { - return this.getTokenFromRequestBody(request); - } - - throw new UnauthorizedRequestError('Unauthorized request: no authentication given'); - } - - /** - * Get the token from the request header. - * - * @param request {Request} - * @see {http://tools.ietf.org/html/rfc6750#section-2.1} - */ - - getTokenFromRequestHeader (request) { - const token = request.get('Authorization'); - const matches = token.match(/^Bearer ([0-9a-zA-Z-._~+/]+=*)$/); - - if (!matches) { - throw new InvalidRequestError('Invalid request: malformed authorization header'); - } - - return matches[1]; - } - - /** - * Get the token from the request query. - * - * "Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page - * URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be - * passed in HTTP message headers or message bodies for which confidentiality measures - * are taken. Browsers, web servers, and other software may not adequately secure URLs - * in the browser history, web server logs, and other data structures. If bearer tokens - * are passed in page URLs, attackers might be able to steal them from the history data, - * logs, or other unsecured locations." - * - * @param request {Request} - * @see http://tools.ietf.org/html/rfc6750#section-2.3 - */ - - getTokenFromRequestQuery (request) { - if (!this.allowBearerTokensInQueryString) { - throw new InvalidRequestError('Invalid request: do not send bearer tokens in query URLs'); - } - - return request.query.access_token; - } - - /** - * Get the token from the request body. - * - * "The HTTP request method is one for which the request-body has defined semantics. - * In particular, this means that the "GET" method MUST NOT be used." - * @param request {Request} - * @see http://tools.ietf.org/html/rfc6750#section-2.2 - */ - - getTokenFromRequestBody (request) { - if (request.method === 'GET') { - throw new InvalidRequestError('Invalid request: token may not be passed in the body when using the GET verb'); - } - - if (!request.is('application/x-www-form-urlencoded')) { - throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded'); - } - - return request.body.access_token; - } - - /** - * Get the access token from the model. - * @param token - */ - - async getAccessToken (token) { - const accessToken = await this.model.getAccessToken(token); - - if (!accessToken) { - throw new InvalidTokenError('Invalid token: access token is invalid'); - } - - if (!accessToken.user) { - throw new ServerError('Server error: `getAccessToken()` did not return a `user` object'); - } - - return accessToken; - } - - /** - * Validate access token. - */ - - validateAccessToken (accessToken) { - if (!(accessToken.accessTokenExpiresAt instanceof Date)) { - throw new ServerError('Server error: `accessTokenExpiresAt` must be a Date instance'); - } - - if (accessToken.accessTokenExpiresAt < new Date()) { - throw new InvalidTokenError('Invalid token: access token has expired'); - } - - return accessToken; - } - - /** - * Verify scope. - */ - - async verifyScope (accessToken) { - const scope = await this.model.verifyScope(accessToken, this.scope); - - if (!scope) { - throw new InsufficientScopeError('Insufficient scope: authorized scope is insufficient'); - } - } - - /** - * Update response. - */ - - updateResponse (response, accessToken) { - if (accessToken.scope == null) { - return; - } - - if (this.scope && this.addAcceptedScopesHeader) { - response.set('X-Accepted-OAuth-Scopes', this.scope.join(' ')); - } - - if (this.scope && this.addAuthorizedScopesHeader) { - response.set('X-OAuth-Scopes', accessToken.scope.join(' ')); - } - } + /** + * @constructor + * @param options {object} Server options. + * @param options.model {Model} The Model; this is always required. + * @param options.scope {string[]|undefined} The scope(s) to authenticate. + * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. + * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. + * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. + * @throws {InvalidArgumentError} if {model} is missing or does not implement `getAccessToken` + */ + constructor(options) { + options = options || {}; + + if (!options.model) { + throw new InvalidArgumentError("Missing parameter: `model`"); + } + + if (!options.model.getAccessToken) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `getAccessToken()`", + ); + } + + if (options.scope && undefined === options.addAcceptedScopesHeader) { + throw new InvalidArgumentError( + "Missing parameter: `addAcceptedScopesHeader`", + ); + } + + if (options.scope && undefined === options.addAuthorizedScopesHeader) { + throw new InvalidArgumentError( + "Missing parameter: `addAuthorizedScopesHeader`", + ); + } + + if (options.scope && !options.model.verifyScope) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `verifyScope()`", + ); + } + + this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; + this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; + this.allowBearerTokensInQueryString = + options.allowBearerTokensInQueryString; + this.model = options.model; + this.scope = Array.isArray(options.scope) + ? options.scope + : parseScope(options.scope); + } + + /** + * Handles the authentication + * @param request {Request} + * @param response {Response} + * @return {Promise<*>} + */ + async handle(request, response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + "Invalid argument: `request` must be an instance of Request", + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + "Invalid argument: `response` must be an instance of Response", + ); + } + + try { + const requestToken = await this.getTokenFromRequest(request); + + let accessToken; + accessToken = await this.getAccessToken(requestToken); + accessToken = await this.validateAccessToken(accessToken); + + if (this.scope) { + await this.verifyScope(accessToken); + } + + this.updateResponse(response, accessToken); + + return accessToken; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // lacks any authentication information. + // + // @see https://tools.ietf.org/html/rfc6750#section-3.1 + if (e instanceof UnauthorizedRequestError) { + response.set("WWW-Authenticate", 'Bearer realm="Service"'); + } else if (e instanceof InvalidRequestError) { + response.set( + "WWW-Authenticate", + 'Bearer realm="Service",error="invalid_request"', + ); + } else if (e instanceof InvalidTokenError) { + response.set( + "WWW-Authenticate", + 'Bearer realm="Service",error="invalid_token"', + ); + } else if (e instanceof InsufficientScopeError) { + response.set( + "WWW-Authenticate", + 'Bearer realm="Service",error="insufficient_scope"', + ); + } + + if (!(e instanceof OAuthError)) { + throw new ServerError(e); + } + + throw e; + } + } + + /** + * Get the token from the header or body, depending on the request. + * + * "Clients MUST NOT use more than one method to transmit the token in each request." + * + * @param {Request} + * @see {https://tools.ietf.org/html/rfc6750#section-2} + */ + getTokenFromRequest(request) { + const headerToken = request.get("Authorization"); + const queryToken = request.query.access_token; + const bodyToken = request.body.access_token; + + if (!!headerToken + !!queryToken + !!bodyToken > 1) { + throw new InvalidRequestError( + "Invalid request: only one authentication method is allowed", + ); + } + + if (headerToken) { + return this.getTokenFromRequestHeader(request); + } + + if (queryToken) { + return this.getTokenFromRequestQuery(request); + } + + if (bodyToken) { + return this.getTokenFromRequestBody(request); + } + + throw new UnauthorizedRequestError( + "Unauthorized request: no authentication given", + ); + } + + /** + * Get the token from the request header. + * + * @param request {Request} + * @see {http://tools.ietf.org/html/rfc6750#section-2.1} + */ + + getTokenFromRequestHeader(request) { + const token = request.get("Authorization"); + const matches = token.match(/^Bearer ([0-9a-zA-Z-._~+/]+=*)$/); + + if (!matches) { + throw new InvalidRequestError( + "Invalid request: malformed authorization header", + ); + } + + return matches[1]; + } + + /** + * Get the token from the request query. + * + * "Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page + * URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be + * passed in HTTP message headers or message bodies for which confidentiality measures + * are taken. Browsers, web servers, and other software may not adequately secure URLs + * in the browser history, web server logs, and other data structures. If bearer tokens + * are passed in page URLs, attackers might be able to steal them from the history data, + * logs, or other unsecured locations." + * + * @param request {Request} + * @see http://tools.ietf.org/html/rfc6750#section-2.3 + */ + + getTokenFromRequestQuery(request) { + if (!this.allowBearerTokensInQueryString) { + throw new InvalidRequestError( + "Invalid request: do not send bearer tokens in query URLs", + ); + } + + return request.query.access_token; + } + + /** + * Get the token from the request body. + * + * "The HTTP request method is one for which the request-body has defined semantics. + * In particular, this means that the "GET" method MUST NOT be used." + * @param request {Request} + * @see http://tools.ietf.org/html/rfc6750#section-2.2 + */ + + getTokenFromRequestBody(request) { + if (request.method === "GET") { + throw new InvalidRequestError( + "Invalid request: token may not be passed in the body when using the GET verb", + ); + } + + if (!request.is("application/x-www-form-urlencoded")) { + throw new InvalidRequestError( + "Invalid request: content must be application/x-www-form-urlencoded", + ); + } + + return request.body.access_token; + } + + /** + * Get the access token from the model. + * @param token + */ + + async getAccessToken(token) { + const accessToken = await this.model.getAccessToken(token); + + if (!accessToken) { + throw new InvalidTokenError("Invalid token: access token is invalid"); + } + + if (!accessToken.user) { + throw new ServerError( + "Server error: `getAccessToken()` did not return a `user` object", + ); + } + + return accessToken; + } + + /** + * Validate access token. + */ + + validateAccessToken(accessToken) { + if (!(accessToken.accessTokenExpiresAt instanceof Date)) { + throw new ServerError( + "Server error: `accessTokenExpiresAt` must be a Date instance", + ); + } + + if (accessToken.accessTokenExpiresAt < new Date()) { + throw new InvalidTokenError("Invalid token: access token has expired"); + } + + return accessToken; + } + + /** + * Verify scope. + */ + + async verifyScope(accessToken) { + const scope = await this.model.verifyScope(accessToken, this.scope); + + if (!scope) { + throw new InsufficientScopeError( + "Insufficient scope: authorized scope is insufficient", + ); + } + } + + /** + * Update response. + */ + + updateResponse(response, accessToken) { + if (accessToken.scope == null) { + return; + } + + if (this.scope && this.addAcceptedScopesHeader) { + response.set("X-Accepted-OAuth-Scopes", this.scope.join(" ")); + } + + if (this.scope && this.addAuthorizedScopesHeader) { + response.set("X-OAuth-Scopes", accessToken.scope.join(" ")); + } + } } module.exports = AuthenticateHandler; diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index c5f0db17..af2115fe 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -1,34 +1,34 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const AccessDeniedError = require('../errors/access-denied-error'); -const AuthenticateHandler = require('../handlers/authenticate-handler'); -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const InvalidClientError = require('../errors/invalid-client-error'); -const InvalidRequestError = require('../errors/invalid-request-error'); -const InvalidScopeError = require('../errors/invalid-scope-error'); -const UnsupportedResponseTypeError = require('../errors/unsupported-response-type-error'); -const OAuthError = require('../errors/oauth-error'); -const Request = require('../request'); -const Response = require('../response'); -const ServerError = require('../errors/server-error'); -const UnauthorizedClientError = require('../errors/unauthorized-client-error'); -const isFormat = require('@node-oauth/formats'); -const tokenUtil = require('../utils/token-util'); -const url = require('url'); -const pkce = require('../pkce/pkce'); -const { parseScope } = require('../utils/scope-util'); +const AccessDeniedError = require("../errors/access-denied-error"); +const AuthenticateHandler = require("../handlers/authenticate-handler"); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const InvalidClientError = require("../errors/invalid-client-error"); +const InvalidRequestError = require("../errors/invalid-request-error"); +const InvalidScopeError = require("../errors/invalid-scope-error"); +const UnsupportedResponseTypeError = require("../errors/unsupported-response-type-error"); +const OAuthError = require("../errors/oauth-error"); +const Request = require("../request"); +const Response = require("../response"); +const ServerError = require("../errors/server-error"); +const UnauthorizedClientError = require("../errors/unauthorized-client-error"); +const isFormat = require("@node-oauth/formats"); +const tokenUtil = require("../utils/token-util"); +const url = require("url"); +const pkce = require("../pkce/pkce"); +const { parseScope } = require("../utils/scope-util"); /** * Response types. */ const responseTypes = { - code: require('../response-types/code-response-type'), - //token: require('../response-types/token-response-type') + code: require("../response-types/code-response-type"), + //token: require('../response-types/token-response-type') }; /** @@ -36,374 +36,441 @@ const responseTypes = { */ class AuthorizeHandler { - constructor (options) { - options = options || {}; - - if (options.authenticateHandler && !options.authenticateHandler.handle) { - throw new InvalidArgumentError('Invalid argument: authenticateHandler does not implement `handle()`'); - } - - if (!options.authorizationCodeLifetime) { - throw new InvalidArgumentError('Missing parameter: `authorizationCodeLifetime`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.model.getClient) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); - } - - if (!options.model.saveAuthorizationCode) { - throw new InvalidArgumentError('Invalid argument: model does not implement `saveAuthorizationCode()`'); - } - - this.allowEmptyState = options.allowEmptyState; - this.authenticateHandler = options.authenticateHandler || new AuthenticateHandler(options); - this.authorizationCodeLifetime = options.authorizationCodeLifetime; - this.enablePlainPKCE = options.enablePlainPKCE === true; - this.model = options.model; - } - - /** - * Authorize Handler. - */ - - async handle (request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); - } - - const expiresAt = await this.getAuthorizationCodeLifetime(); - const client = await this.getClient(request); - const user = await this.getUser(request, response); - - let uri; - let state; - - try { - uri = this.getRedirectUri(request, client); - state = this.getState(request); - - if (request.query.allowed === 'false' || request.body.allowed === 'false') { - throw new AccessDeniedError('Access denied: user denied access to application'); - } - - const requestedScope = await this.getScope(request); - const validScope = await this.validateScope(user, client, requestedScope); - const authorizationCode = await this.generateAuthorizationCode(client, user, validScope); - - const ResponseType = this.getResponseType(request); - const codeChallenge = this.getCodeChallenge(request); - const codeChallengeMethod = this.getCodeChallengeMethod(request); - const code = await this.saveAuthorizationCode( - authorizationCode, - expiresAt, - validScope, - client, - uri, - user, - codeChallenge, - codeChallengeMethod - ); - - const responseTypeInstance = new ResponseType(code.authorizationCode); - const redirectUri = this.buildSuccessRedirectUri(uri, responseTypeInstance); - - this.updateResponse(response, redirectUri, state); - - return code; - } catch (err) { - let e = err; - - if (!(e instanceof OAuthError)) { - e = new ServerError(e); - } - const redirectUri = this.buildErrorRedirectUri(uri, e); - this.updateResponse(response, redirectUri, state); - - throw e; - } - } - - /** - * Generate authorization code. - */ - - async generateAuthorizationCode (client, user, scope) { - if (this.model.generateAuthorizationCode) { - return this.model.generateAuthorizationCode(client, user, scope); - } - return tokenUtil.generateRandomToken(); - } - - /** - * Get authorization code lifetime. - */ - - getAuthorizationCodeLifetime () { - const expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.authorizationCodeLifetime); - return expires; - } - - /** - * Get the client from the model. - */ - - async getClient (request) { - const self = this; - const clientId = request.body.client_id || request.query.client_id; - - if (!clientId) { - throw new InvalidRequestError('Missing parameter: `client_id`'); - } - - if (!isFormat.vschar(clientId)) { - throw new InvalidRequestError('Invalid parameter: `client_id`'); - } - - const redirectUri = request.body.redirect_uri || request.query.redirect_uri; - - if (redirectUri && !isFormat.uri(redirectUri)) { - throw new InvalidRequestError('Invalid request: `redirect_uri` is not a valid URI'); - } - - const client = await this.model.getClient(clientId, null); - - if (!client) { - throw new InvalidClientError('Invalid client: client credentials are invalid'); - } - - if (!client.grants) { - throw new InvalidClientError('Invalid client: missing client `grants`'); - } - - if (!Array.isArray(client.grants) || !client.grants.includes('authorization_code')) { - throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); - } - - if (!client.redirectUris || 0 === client.redirectUris.length) { - throw new InvalidClientError('Invalid client: missing client `redirectUri`'); - } - - if (redirectUri) { - const valid = await self.validateRedirectUri(redirectUri, client); - - if (!valid) { - throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value'); - } - } - - return client; - } - - /** - * Validate requested scope. - */ - async validateScope (user, client, scope) { - if (this.model.validateScope) { - const validatedScope = await this.model.validateScope(user, client, scope); - - if (!validatedScope) { - throw new InvalidScopeError('Invalid scope: Requested scope is invalid'); - } - - return validatedScope; - } - - return scope; - } - - /** - * Get scope from the request. - */ - - getScope (request) { - const scope = request.body.scope || request.query.scope; - - return parseScope(scope); - } - - /** - * Get state from the request. - */ - - getState (request) { - const state = request.body.state || request.query.state; - const stateExists = state && state.length > 0; - const stateIsValid = stateExists - ? isFormat.vschar(state) - : this.allowEmptyState; - - if (!stateIsValid) { - const message = (!stateExists) ? 'Missing' : 'Invalid'; - throw new InvalidRequestError(`${message} parameter: \`state\``); - } - - return state; - } - - /** - * Get user by calling the authenticate middleware. - */ - - async getUser (request, response) { - if (this.authenticateHandler instanceof AuthenticateHandler) { - const handled = await this.authenticateHandler.handle(request, response); - return handled - ? handled.user - : undefined; - } - - const user = await this.authenticateHandler.handle(request, response); - - if (!user) { - throw new ServerError('Server error: `handle()` did not return a `user` object'); - } - - return user; - } - - /** - * Get redirect URI. - */ - - getRedirectUri (request, client) { - return request.body.redirect_uri || request.query.redirect_uri || client.redirectUris[0]; - } - - /** - * Save authorization code. - */ - - async saveAuthorizationCode (authorizationCode, expiresAt, scope, client, redirectUri, user, codeChallenge, codeChallengeMethod) { - let code = { - authorizationCode: authorizationCode, - expiresAt: expiresAt, - redirectUri: redirectUri, - scope: scope - }; - - if(codeChallenge && codeChallengeMethod){ - code = Object.assign({ - codeChallenge: codeChallenge, - codeChallengeMethod: codeChallengeMethod - }, code); - } - - return this.model.saveAuthorizationCode(code, client, user); - } - - - async validateRedirectUri (redirectUri, client) { - if (this.model.validateRedirectUri) { - return this.model.validateRedirectUri(redirectUri, client); - } - - return client.redirectUris.includes(redirectUri); - } - /** - * Get response type. - */ - - getResponseType (request) { - const responseType = request.body.response_type || request.query.response_type; - - if (!responseType) { - throw new InvalidRequestError('Missing parameter: `response_type`'); - } - - if (!Object.prototype.hasOwnProperty.call(responseTypes, responseType)) { - throw new UnsupportedResponseTypeError('Unsupported response type: `response_type` is not supported'); - } - - return responseTypes[responseType]; - } - - /** - * Build a successful response that redirects the user-agent to the client-provided url. - */ - - buildSuccessRedirectUri (redirectUri, responseType) { - return responseType.buildRedirectUri(redirectUri); - } - - /** - * Build an error response that redirects the user-agent to the client-provided url. - */ - - buildErrorRedirectUri (redirectUri, error) { - const uri = url.parse(redirectUri); - - uri.query = { - error: error.name - }; - - if (error.message) { - uri.query.error_description = error.message; - } - - return uri; - } - - /** - * Update response with the redirect uri and the state parameter, if available. - */ - - updateResponse (response, redirectUri, state) { - redirectUri.query = redirectUri.query || {}; - - if (state) { - redirectUri.query.state = state; - } - - response.redirect(url.format(redirectUri)); - } - - getCodeChallenge (request) { - return request.body.code_challenge || request.query.code_challenge; - } - - /** - * Get code challenge method from request. - * - * When `enablePlainPKCE` is false (the default), the "plain" method is - * rejected and the default (when no method is provided) is "S256". - * When `enablePlainPKCE` is true, "plain" is accepted and used as the - * default per RFC 7636 §4.3. - * - * @see https://www.rfc-editor.org/rfc/rfc7636#section-4.3 - * @throws {InvalidRequestError} if request contains unsupported code_challenge_method - * (see https://www.rfc-editor.org/rfc/rfc7636#section-4.4) - */ - getCodeChallengeMethod (request) { - const algorithm = request.body.code_challenge_method || request.query.code_challenge_method; - - if (algorithm && !pkce.isValidMethod(algorithm)) { - throw new InvalidRequestError(`Invalid request: transform algorithm '${algorithm}' not supported`); - } - - if (!this.enablePlainPKCE && algorithm === 'plain') { - throw new InvalidRequestError('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); - } - - // return the verified algorithm, if provided - if (algorithm) { - return algorithm; - } - - // otherwise, return the default algorithm based on the value of `enablePlainPKCE` - // which enables extended hardening by default, while - // optionally enable legacy support for the "plain" method per RFC 7636 §4.3 - return this.enablePlainPKCE ? 'plain' : 'S256'; - } + constructor(options) { + options = options || {}; + + if (options.authenticateHandler && !options.authenticateHandler.handle) { + throw new InvalidArgumentError( + "Invalid argument: authenticateHandler does not implement `handle()`", + ); + } + + if (!options.authorizationCodeLifetime) { + throw new InvalidArgumentError( + "Missing parameter: `authorizationCodeLifetime`", + ); + } + + if (!options.model) { + throw new InvalidArgumentError("Missing parameter: `model`"); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `getClient()`", + ); + } + + if (!options.model.saveAuthorizationCode) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `saveAuthorizationCode()`", + ); + } + + this.allowEmptyState = options.allowEmptyState; + this.authenticateHandler = + options.authenticateHandler || new AuthenticateHandler(options); + this.authorizationCodeLifetime = options.authorizationCodeLifetime; + this.enablePlainPKCE = options.enablePlainPKCE === true; + this.model = options.model; + } + + /** + * Authorize Handler. + */ + + async handle(request, response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + "Invalid argument: `request` must be an instance of Request", + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + "Invalid argument: `response` must be an instance of Response", + ); + } + + const expiresAt = await this.getAuthorizationCodeLifetime(); + const client = await this.getClient(request); + const user = await this.getUser(request, response); + + let uri; + let state; + + try { + uri = this.getRedirectUri(request, client); + state = this.getState(request); + + if ( + request.query.allowed === "false" || + request.body.allowed === "false" + ) { + throw new AccessDeniedError( + "Access denied: user denied access to application", + ); + } + + const requestedScope = await this.getScope(request); + const validScope = await this.validateScope(user, client, requestedScope); + const authorizationCode = await this.generateAuthorizationCode( + client, + user, + validScope, + ); + + const ResponseType = this.getResponseType(request); + const codeChallenge = this.getCodeChallenge(request); + const codeChallengeMethod = this.getCodeChallengeMethod(request); + const code = await this.saveAuthorizationCode( + authorizationCode, + expiresAt, + validScope, + client, + uri, + user, + codeChallenge, + codeChallengeMethod, + ); + + const responseTypeInstance = new ResponseType(code.authorizationCode); + const redirectUri = this.buildSuccessRedirectUri( + uri, + responseTypeInstance, + ); + + this.updateResponse(response, redirectUri, state); + + return code; + } catch (err) { + let e = err; + + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + const redirectUri = this.buildErrorRedirectUri(uri, e); + this.updateResponse(response, redirectUri, state); + + throw e; + } + } + + /** + * Generate authorization code. + */ + + async generateAuthorizationCode(client, user, scope) { + if (this.model.generateAuthorizationCode) { + return this.model.generateAuthorizationCode(client, user, scope); + } + return tokenUtil.generateRandomToken(); + } + + /** + * Get authorization code lifetime. + */ + + getAuthorizationCodeLifetime() { + const expires = new Date(); + + expires.setSeconds(expires.getSeconds() + this.authorizationCodeLifetime); + return expires; + } + + /** + * Get the client from the model. + */ + + async getClient(request) { + const self = this; + const clientId = request.body.client_id || request.query.client_id; + + if (!clientId) { + throw new InvalidRequestError("Missing parameter: `client_id`"); + } + + if (!isFormat.vschar(clientId)) { + throw new InvalidRequestError("Invalid parameter: `client_id`"); + } + + const redirectUri = request.body.redirect_uri || request.query.redirect_uri; + + if (redirectUri && !isFormat.uri(redirectUri)) { + throw new InvalidRequestError( + "Invalid request: `redirect_uri` is not a valid URI", + ); + } + + const client = await this.model.getClient(clientId, null); + + if (!client) { + throw new InvalidClientError( + "Invalid client: client credentials are invalid", + ); + } + + if (!client.grants) { + throw new InvalidClientError("Invalid client: missing client `grants`"); + } + + if ( + !Array.isArray(client.grants) || + !client.grants.includes("authorization_code") + ) { + throw new UnauthorizedClientError( + "Unauthorized client: `grant_type` is invalid", + ); + } + + if (!client.redirectUris || 0 === client.redirectUris.length) { + throw new InvalidClientError( + "Invalid client: missing client `redirectUri`", + ); + } + + if (redirectUri) { + const valid = await self.validateRedirectUri(redirectUri, client); + + if (!valid) { + throw new InvalidClientError( + "Invalid client: `redirect_uri` does not match client value", + ); + } + } + + return client; + } + + /** + * Validate requested scope. + */ + async validateScope(user, client, scope) { + if (this.model.validateScope) { + const validatedScope = await this.model.validateScope( + user, + client, + scope, + ); + + if (!validatedScope) { + throw new InvalidScopeError( + "Invalid scope: Requested scope is invalid", + ); + } + + return validatedScope; + } + + return scope; + } + + /** + * Get scope from the request. + */ + + getScope(request) { + const scope = request.body.scope || request.query.scope; + + return parseScope(scope); + } + + /** + * Get state from the request. + */ + + getState(request) { + const state = request.body.state || request.query.state; + const stateExists = state && state.length > 0; + const stateIsValid = stateExists + ? isFormat.vschar(state) + : this.allowEmptyState; + + if (!stateIsValid) { + const message = !stateExists ? "Missing" : "Invalid"; + throw new InvalidRequestError(`${message} parameter: \`state\``); + } + + return state; + } + + /** + * Get user by calling the authenticate middleware. + */ + + async getUser(request, response) { + if (this.authenticateHandler instanceof AuthenticateHandler) { + const handled = await this.authenticateHandler.handle(request, response); + return handled ? handled.user : undefined; + } + + const user = await this.authenticateHandler.handle(request, response); + + if (!user) { + throw new ServerError( + "Server error: `handle()` did not return a `user` object", + ); + } + + return user; + } + + /** + * Get redirect URI. + */ + + getRedirectUri(request, client) { + return ( + request.body.redirect_uri || + request.query.redirect_uri || + client.redirectUris[0] + ); + } + + /** + * Save authorization code. + */ + + async saveAuthorizationCode( + authorizationCode, + expiresAt, + scope, + client, + redirectUri, + user, + codeChallenge, + codeChallengeMethod, + ) { + let code = { + authorizationCode: authorizationCode, + expiresAt: expiresAt, + redirectUri: redirectUri, + scope: scope, + }; + + if (codeChallenge && codeChallengeMethod) { + code = Object.assign( + { + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod, + }, + code, + ); + } + + return this.model.saveAuthorizationCode(code, client, user); + } + + async validateRedirectUri(redirectUri, client) { + if (this.model.validateRedirectUri) { + return this.model.validateRedirectUri(redirectUri, client); + } + + return client.redirectUris.includes(redirectUri); + } + /** + * Get response type. + */ + + getResponseType(request) { + const responseType = + request.body.response_type || request.query.response_type; + + if (!responseType) { + throw new InvalidRequestError("Missing parameter: `response_type`"); + } + + if (!Object.prototype.hasOwnProperty.call(responseTypes, responseType)) { + throw new UnsupportedResponseTypeError( + "Unsupported response type: `response_type` is not supported", + ); + } + + return responseTypes[responseType]; + } + + /** + * Build a successful response that redirects the user-agent to the client-provided url. + */ + + buildSuccessRedirectUri(redirectUri, responseType) { + return responseType.buildRedirectUri(redirectUri); + } + + /** + * Build an error response that redirects the user-agent to the client-provided url. + */ + + buildErrorRedirectUri(redirectUri, error) { + const uri = url.parse(redirectUri); + + uri.query = { + error: error.name, + }; + + if (error.message) { + uri.query.error_description = error.message; + } + + return uri; + } + + /** + * Update response with the redirect uri and the state parameter, if available. + */ + + updateResponse(response, redirectUri, state) { + redirectUri.query = redirectUri.query || {}; + + if (state) { + redirectUri.query.state = state; + } + + response.redirect(url.format(redirectUri)); + } + + getCodeChallenge(request) { + return request.body.code_challenge || request.query.code_challenge; + } + + /** + * Get code challenge method from request. + * + * When `enablePlainPKCE` is false (the default), the "plain" method is + * rejected and the default (when no method is provided) is "S256". + * When `enablePlainPKCE` is true, "plain" is accepted and used as the + * default per RFC 7636 §4.3. + * + * @see https://www.rfc-editor.org/rfc/rfc7636#section-4.3 + * @throws {InvalidRequestError} if request contains unsupported code_challenge_method + * (see https://www.rfc-editor.org/rfc/rfc7636#section-4.4) + */ + getCodeChallengeMethod(request) { + const algorithm = + request.body.code_challenge_method || request.query.code_challenge_method; + + if (algorithm && !pkce.isValidMethod(algorithm)) { + throw new InvalidRequestError( + `Invalid request: transform algorithm '${algorithm}' not supported`, + ); + } + + if (!this.enablePlainPKCE && algorithm === "plain") { + throw new InvalidRequestError( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } + + // return the verified algorithm, if provided + if (algorithm) { + return algorithm; + } + + // otherwise, return the default algorithm based on the value of `enablePlainPKCE` + // which enables extended hardening by default, while + // optionally enable legacy support for the "plain" method per RFC 7636 §4.3 + return this.enablePlainPKCE ? "plain" : "S256"; + } } /** diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index 3b983072..d406d587 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -1,33 +1,33 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const BearerTokenType = require('../token-types/bearer-token-type'); -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const InvalidClientError = require('../errors/invalid-client-error'); -const InvalidRequestError = require('../errors/invalid-request-error'); -const OAuthError = require('../errors/oauth-error'); -const Request = require('../request'); -const Response = require('../response'); -const ServerError = require('../errors/server-error'); -const TokenModel = require('../models/token-model'); -const UnauthorizedClientError = require('../errors/unauthorized-client-error'); -const UnsupportedGrantTypeError = require('../errors/unsupported-grant-type-error'); -const auth = require('basic-auth'); -const pkce = require('../pkce/pkce'); -const isFormat = require('@node-oauth/formats'); +const BearerTokenType = require("../token-types/bearer-token-type"); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const InvalidClientError = require("../errors/invalid-client-error"); +const InvalidRequestError = require("../errors/invalid-request-error"); +const OAuthError = require("../errors/oauth-error"); +const Request = require("../request"); +const Response = require("../response"); +const ServerError = require("../errors/server-error"); +const TokenModel = require("../models/token-model"); +const UnauthorizedClientError = require("../errors/unauthorized-client-error"); +const UnsupportedGrantTypeError = require("../errors/unsupported-grant-type-error"); +const auth = require("basic-auth"); +const pkce = require("../pkce/pkce"); +const isFormat = require("@node-oauth/formats"); /** * Grant types. */ const grantTypes = { - authorization_code: require('../grant-types/authorization-code-grant-type'), - client_credentials: require('../grant-types/client-credentials-grant-type'), - password: require('../grant-types/password-grant-type'), - refresh_token: require('../grant-types/refresh-token-grant-type') + authorization_code: require("../grant-types/authorization-code-grant-type"), + client_credentials: require("../grant-types/client-credentials-grant-type"), + password: require("../grant-types/password-grant-type"), + refresh_token: require("../grant-types/refresh-token-grant-type"), }; /** @@ -35,272 +35,315 @@ const grantTypes = { */ class TokenHandler { - constructor (options) { - options = options || {}; - - if (!options.accessTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); - } - - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } - - if (!options.refreshTokenLifetime) { - throw new InvalidArgumentError('Missing parameter: `refreshTokenLifetime`'); - } - - if (!options.model.getClient) { - throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); - } - - this.accessTokenLifetime = options.accessTokenLifetime; - this.grantTypes = Object.assign({}, grantTypes, options.extendedGrantTypes); - this.model = options.model; - this.refreshTokenLifetime = options.refreshTokenLifetime; - this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; - this.requireClientAuthentication = options.requireClientAuthentication || {}; - this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken !== false; - this.enablePlainPKCE = options.enablePlainPKCE === true; - } - - /** - * Token Handler. - */ - - async handle (request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); - } - - if (request.method !== 'POST') { - throw new InvalidRequestError('Invalid request: method must be POST'); - } - - if (!request.is('application/x-www-form-urlencoded')) { - throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded'); - } - - try { - const client = await this.getClient(request, response); - const data = await this.handleGrantType(request, client); - const model = new TokenModel(data, { allowExtendedTokenAttributes: this.allowExtendedTokenAttributes }); - const tokenType = this.getTokenType(model); - - this.updateSuccessResponse(response, tokenType); - - return data; - } catch (err) { - let e = err; - - if (!(e instanceof OAuthError)) { - e = new ServerError(e); - } - - this.updateErrorResponse(response, e); - throw e; - } - } - - /** - * Get the client from the model. - */ - - async getClient (request, response) { - const credentials = await this.getClientCredentials(request); - const grantType = request.body.grant_type; - const codeVerifier = request.body.code_verifier; - const isPkce = pkce.isPKCERequest({ grantType, codeVerifier }); - - if (!credentials.clientId) { - throw new InvalidRequestError('Missing parameter: `client_id`'); - } - - if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret && !isPkce) { - throw new InvalidRequestError('Missing parameter: `client_secret`'); - } - - if (!isFormat.vschar(credentials.clientId)) { - throw new InvalidRequestError('Invalid parameter: `client_id`'); - } - - if (credentials.clientSecret && !isFormat.vschar(credentials.clientSecret)) { - throw new InvalidRequestError('Invalid parameter: `client_secret`'); - } - - try { - const client = await this.model.getClient(credentials.clientId, credentials.clientSecret); - - if (!client) { - throw new InvalidClientError('Invalid client: client is invalid'); - } - - if (!client.grants) { - throw new ServerError('Server error: missing client `grants`'); - } - - if (!(client.grants instanceof Array)) { - throw new ServerError('Server error: `grants` must be an array'); - } - - return client; - } catch (e) { - // Include the "WWW-Authenticate" response header field if the client - // attempted to authenticate via the "Authorization" request header. - // - // @see https://tools.ietf.org/html/rfc6749#section-5.2. - if ((e instanceof InvalidClientError) && request.get('authorization')) { - response.set('WWW-Authenticate', 'Basic realm="Service"'); - throw new InvalidClientError(e, { code: 401 }); - } - - throw e; - } - } - - /** - * Get client credentials. - * - * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, - * the `client_id` and `client_secret` can be embedded in the body. - * - * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 - */ - - getClientCredentials (request) { - const credentials = auth(request); - const grantType = request.body.grant_type; - const codeVerifier = request.body.code_verifier; - - if (credentials) { - return { clientId: credentials.name, clientSecret: credentials.pass }; - } - - if (request.body.client_id && request.body.client_secret) { - return { clientId: request.body.client_id, clientSecret: request.body.client_secret }; - } - - if (pkce.isPKCERequest({ grantType, codeVerifier })) { - if(request.body.client_id) { - return { clientId: request.body.client_id }; - } - } - - if (!this.isClientAuthenticationRequired(grantType)) { - if(request.body.client_id) { - return { clientId: request.body.client_id }; - } - } - - throw new InvalidClientError('Invalid client: cannot retrieve client credentials'); - } - - /** - * Handle grant type. - */ - - async handleGrantType (request, client) { - const grantType = request.body.grant_type; - - if (!grantType) { - throw new InvalidRequestError('Missing parameter: `grant_type`'); - } - - if (!isFormat.nchar(grantType) && !isFormat.uri(grantType)) { - throw new InvalidRequestError('Invalid parameter: `grant_type`'); - } - - if (!Object.prototype.hasOwnProperty.call(this.grantTypes, grantType)) { - throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid'); - } - - if (!Array.isArray(client.grants) || !client.grants.includes(grantType)) { - throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); - } - - const accessTokenLifetime = this.getAccessTokenLifetime(client); - const refreshTokenLifetime = this.getRefreshTokenLifetime(client); - const Type = this.grantTypes[grantType]; - - const options = { - accessTokenLifetime: accessTokenLifetime, - model: this.model, - refreshTokenLifetime: refreshTokenLifetime, - alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken, - enablePlainPKCE: this.enablePlainPKCE === true - }; - - return new Type(options).handle(request, client); - } - - /** - * Get access token lifetime. - */ - - getAccessTokenLifetime (client) { - return client.accessTokenLifetime || this.accessTokenLifetime; - } - - /** - * Get refresh token lifetime. - */ - - getRefreshTokenLifetime (client) { - return client.refreshTokenLifetime || this.refreshTokenLifetime; - } - - /** - * Get token type. - */ - - getTokenType (model) { - return new BearerTokenType(model.accessToken, model.accessTokenLifetime, model.refreshToken, model.scope, model.customAttributes); - } - - /** - * Update response when a token is generated. - */ - - updateSuccessResponse (response, tokenType) { - response.body = tokenType.valueOf(); - - // for compliance reasons we rebuild the internal scope to be a string - // https://datatracker.ietf.org/doc/html/rfc6749.html#section-5.1 - if (response.body.scope) { - response.body.scope = response.body.scope.join(' '); - } - - response.set('Cache-Control', 'no-store'); - response.set('Pragma', 'no-cache'); - } - - /** - * Update response when an error is thrown. - */ - - updateErrorResponse (response, error) { - response.body = { - error: error.name, - error_description: error.message - }; - - response.status = error.code; - } - - /** - * Given a grant type, check if client authentication is required - */ - isClientAuthenticationRequired (grantType) { - if (Object.keys(this.requireClientAuthentication).length > 0) { - return (typeof this.requireClientAuthentication[grantType] !== 'undefined') ? this.requireClientAuthentication[grantType] : true; - } else { - return true; - } - } + constructor(options) { + options = options || {}; + + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + "Missing parameter: `accessTokenLifetime`", + ); + } + + if (!options.model) { + throw new InvalidArgumentError("Missing parameter: `model`"); + } + + if (!options.refreshTokenLifetime) { + throw new InvalidArgumentError( + "Missing parameter: `refreshTokenLifetime`", + ); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + "Invalid argument: model does not implement `getClient()`", + ); + } + + this.accessTokenLifetime = options.accessTokenLifetime; + this.grantTypes = Object.assign({}, grantTypes, options.extendedGrantTypes); + this.model = options.model; + this.refreshTokenLifetime = options.refreshTokenLifetime; + this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; + this.requireClientAuthentication = + options.requireClientAuthentication || {}; + this.alwaysIssueNewRefreshToken = + options.alwaysIssueNewRefreshToken !== false; + this.enablePlainPKCE = options.enablePlainPKCE === true; + } + + /** + * Token Handler. + */ + + async handle(request, response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + "Invalid argument: `request` must be an instance of Request", + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + "Invalid argument: `response` must be an instance of Response", + ); + } + + if (request.method !== "POST") { + throw new InvalidRequestError("Invalid request: method must be POST"); + } + + if (!request.is("application/x-www-form-urlencoded")) { + throw new InvalidRequestError( + "Invalid request: content must be application/x-www-form-urlencoded", + ); + } + + try { + const client = await this.getClient(request, response); + const data = await this.handleGrantType(request, client); + const model = new TokenModel(data, { + allowExtendedTokenAttributes: this.allowExtendedTokenAttributes, + }); + const tokenType = this.getTokenType(model); + + this.updateSuccessResponse(response, tokenType); + + return data; + } catch (err) { + let e = err; + + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + + this.updateErrorResponse(response, e); + throw e; + } + } + + /** + * Get the client from the model. + */ + + async getClient(request, response) { + const credentials = await this.getClientCredentials(request); + const grantType = request.body.grant_type; + const codeVerifier = request.body.code_verifier; + const isPkce = pkce.isPKCERequest({ grantType, codeVerifier }); + + if (!credentials.clientId) { + throw new InvalidRequestError("Missing parameter: `client_id`"); + } + + if ( + this.isClientAuthenticationRequired(grantType) && + !credentials.clientSecret && + !isPkce + ) { + throw new InvalidRequestError("Missing parameter: `client_secret`"); + } + + if (!isFormat.vschar(credentials.clientId)) { + throw new InvalidRequestError("Invalid parameter: `client_id`"); + } + + if ( + credentials.clientSecret && + !isFormat.vschar(credentials.clientSecret) + ) { + throw new InvalidRequestError("Invalid parameter: `client_secret`"); + } + + try { + const client = await this.model.getClient( + credentials.clientId, + credentials.clientSecret, + ); + + if (!client) { + throw new InvalidClientError("Invalid client: client is invalid"); + } + + if (!client.grants) { + throw new ServerError("Server error: missing client `grants`"); + } + + if (!(client.grants instanceof Array)) { + throw new ServerError("Server error: `grants` must be an array"); + } + + return client; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // attempted to authenticate via the "Authorization" request header. + // + // @see https://tools.ietf.org/html/rfc6749#section-5.2. + if (e instanceof InvalidClientError && request.get("authorization")) { + response.set("WWW-Authenticate", 'Basic realm="Service"'); + throw new InvalidClientError(e, { code: 401 }); + } + + throw e; + } + } + + /** + * Get client credentials. + * + * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, + * the `client_id` and `client_secret` can be embedded in the body. + * + * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + + getClientCredentials(request) { + const credentials = auth(request); + const grantType = request.body.grant_type; + const codeVerifier = request.body.code_verifier; + + if (credentials) { + return { clientId: credentials.name, clientSecret: credentials.pass }; + } + + if (request.body.client_id && request.body.client_secret) { + return { + clientId: request.body.client_id, + clientSecret: request.body.client_secret, + }; + } + + if (pkce.isPKCERequest({ grantType, codeVerifier })) { + if (request.body.client_id) { + return { clientId: request.body.client_id }; + } + } + + if (!this.isClientAuthenticationRequired(grantType)) { + if (request.body.client_id) { + return { clientId: request.body.client_id }; + } + } + + throw new InvalidClientError( + "Invalid client: cannot retrieve client credentials", + ); + } + + /** + * Handle grant type. + */ + + async handleGrantType(request, client) { + const grantType = request.body.grant_type; + + if (!grantType) { + throw new InvalidRequestError("Missing parameter: `grant_type`"); + } + + if (!isFormat.nchar(grantType) && !isFormat.uri(grantType)) { + throw new InvalidRequestError("Invalid parameter: `grant_type`"); + } + + if (!Object.prototype.hasOwnProperty.call(this.grantTypes, grantType)) { + throw new UnsupportedGrantTypeError( + "Unsupported grant type: `grant_type` is invalid", + ); + } + + if (!Array.isArray(client.grants) || !client.grants.includes(grantType)) { + throw new UnauthorizedClientError( + "Unauthorized client: `grant_type` is invalid", + ); + } + + const accessTokenLifetime = this.getAccessTokenLifetime(client); + const refreshTokenLifetime = this.getRefreshTokenLifetime(client); + const Type = this.grantTypes[grantType]; + + const options = { + accessTokenLifetime: accessTokenLifetime, + model: this.model, + refreshTokenLifetime: refreshTokenLifetime, + alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken, + enablePlainPKCE: this.enablePlainPKCE === true, + }; + + return new Type(options).handle(request, client); + } + + /** + * Get access token lifetime. + */ + + getAccessTokenLifetime(client) { + return client.accessTokenLifetime || this.accessTokenLifetime; + } + + /** + * Get refresh token lifetime. + */ + + getRefreshTokenLifetime(client) { + return client.refreshTokenLifetime || this.refreshTokenLifetime; + } + + /** + * Get token type. + */ + + getTokenType(model) { + return new BearerTokenType( + model.accessToken, + model.accessTokenLifetime, + model.refreshToken, + model.scope, + model.customAttributes, + ); + } + + /** + * Update response when a token is generated. + */ + + updateSuccessResponse(response, tokenType) { + response.body = tokenType.valueOf(); + + // for compliance reasons we rebuild the internal scope to be a string + // https://datatracker.ietf.org/doc/html/rfc6749.html#section-5.1 + if (response.body.scope) { + response.body.scope = response.body.scope.join(" "); + } + + response.set("Cache-Control", "no-store"); + response.set("Pragma", "no-cache"); + } + + /** + * Update response when an error is thrown. + */ + + updateErrorResponse(response, error) { + response.body = { + error: error.name, + error_description: error.message, + }; + + response.status = error.code; + } + + /** + * Given a grant type, check if client authentication is required + */ + isClientAuthenticationRequired(grantType) { + if (Object.keys(this.requireClientAuthentication).length > 0) { + return typeof this.requireClientAuthentication[grantType] !== "undefined" + ? this.requireClientAuthentication[grantType] + : true; + } else { + return true; + } + } } /** diff --git a/lib/model.js b/lib/model.js index 2e3a5eba..eba1f522 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1,9 +1,9 @@ -'use strict'; +"use strict"; /* * Module dependencies */ -const ServerError = require('./errors/server-error'); +const ServerError = require("./errors/server-error"); /** * @typedef AccessTokenData @@ -63,172 +63,171 @@ const ServerError = require('./errors/server-error'); * getClient: () => { ... } * }) */ -class Model { // eslint-disable-line no-unused-vars - /** - * Factory function to create a model form your implementation. - * @static - * @param impl {object} an object containing your model function implementations - * @return {Model} the model instance. - */ - static from (impl) { - const m = new Model(); - const nullFns = {}; - Object - .getOwnPropertyNames(Model.prototype) - .forEach((key) => { - nullFns[key] = null; - }); - Object.assign(m, nullFns, impl); - return m; - } +class Model { + // eslint-disable-line no-unused-vars + /** + * Factory function to create a model form your implementation. + * @static + * @param impl {object} an object containing your model function implementations + * @return {Model} the model instance. + */ + static from(impl) { + const m = new Model(); + const nullFns = {}; + Object.getOwnPropertyNames(Model.prototype).forEach((key) => { + nullFns[key] = null; + }); + Object.assign(m, nullFns, impl); + return m; + } - /*------------------------------------------------------------------------- + /*------------------------------------------------------------------------- | ALWAYS REQUIRED *------------------------------------------------------------------------- | The following functions are required by every workflow / grant type. */ - /** - * Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type. - * This model function is **required** for all grant types. - * **Invoked during:** - * - * - `authorization_code` grant - * - `client_credentials` grant - * - `refresh_token` grant - * - `password` grant - * - * - * @async - * @param clientId {string} The client id of the client to retrieve. - * @param clientSecret {string?} The client secret of the client to retrieve. Can be `null`. - * @returns {Promise.} - * @fulfil {ClientData} - An `Object` representing the client and associated data, or a falsy value if no such client could be found. - * @reject {Error} - An Error type - */ - async getClient(clientId, clientSecret) { - throw new ServerError('getClient not implemented'); - } + /** + * Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type. + * This model function is **required** for all grant types. + * **Invoked during:** + * + * - `authorization_code` grant + * - `client_credentials` grant + * - `refresh_token` grant + * - `password` grant + * + * + * @async + * @param clientId {string} The client id of the client to retrieve. + * @param clientSecret {string?} The client secret of the client to retrieve. Can be `null`. + * @returns {Promise.} + * @fulfil {ClientData} - An `Object` representing the client and associated data, or a falsy value if no such client could be found. + * @reject {Error} - An Error type + */ + async getClient(clientId, clientSecret) { + throw new ServerError("getClient not implemented"); + } - /** - * Invoked to save an access token and optionally a refresh token, depending on the grant type. - * This model function is **required** for all grant types. - * - * **Invoked during:** - * - `authorization_code` grant - * - `client_credentials` grant - * - `refresh_token` grant - * - `password` grant - * - * If the `allowExtendedTokenAttributes` server option is enabled (see `OAuth2Server#token() `) any additional attributes set on the result are copied to the token response sent to the client. - * - * @async - * @instance - * @param token {object} The token(s) to be saved. - * @param token.accessToken {string} The access token to be saved. - * @param token.accessTokenExpiresAt {Date} The expiry time of the access token. - * @param token.refreshToken {string} The refresh token to be saved. - * @param token.refreshTokenExpiresAt {Date} The expiry time of the refresh token. - * @param token.scope {string[]} The authorized scope of the token(s) - * @param client {ClientData} The client associated with the token(s). - * @param user {object} The user associated with the token(s). - * @return {Promise} - * @fulfil {{accessToken:string,accessTokenExpiresAt:Date,refreshToken: string,refreshTokenExpiresAt: Date,scope: string[],client: ClientData,user: object}} An `Object` representing the token(s) and associated data. - * @example - * function saveToken(token, client, user) { - * // imaginary DB queries - * let fns = [ - * db.saveAccessToken({ - * access_token: token.accessToken, - * expires_at: token.accessTokenExpiresAt, - * scope: token.scope, - * client_id: client.id, - * user_id: user.id - * }), - * db.saveRefreshToken({ - * refresh_token: token.refreshToken, - * expires_at: token.refreshTokenExpiresAt, - * scope: token.scope, - * client_id: client.id, - * user_id: user.id - * }) - * ]; - * return Promise.all(fns); - * .spread(function(accessToken, refreshToken) { - * return { - * accessToken: accessToken.access_token, - * accessTokenExpiresAt: accessToken.expires_at, - * refreshToken: refreshToken.refresh_token, - * refreshTokenExpiresAt: refreshToken.expires_at, - * scope: accessToken.scope, - * client: {id: accessToken.client_id}, - * user: {id: accessToken.user_id} - * }; - * }); - * } - */ - async saveToken(token, client, user) { - throw new ServerError('saveToken not implemented'); - } + /** + * Invoked to save an access token and optionally a refresh token, depending on the grant type. + * This model function is **required** for all grant types. + * + * **Invoked during:** + * - `authorization_code` grant + * - `client_credentials` grant + * - `refresh_token` grant + * - `password` grant + * + * If the `allowExtendedTokenAttributes` server option is enabled (see `OAuth2Server#token() `) any additional attributes set on the result are copied to the token response sent to the client. + * + * @async + * @instance + * @param token {object} The token(s) to be saved. + * @param token.accessToken {string} The access token to be saved. + * @param token.accessTokenExpiresAt {Date} The expiry time of the access token. + * @param token.refreshToken {string} The refresh token to be saved. + * @param token.refreshTokenExpiresAt {Date} The expiry time of the refresh token. + * @param token.scope {string[]} The authorized scope of the token(s) + * @param client {ClientData} The client associated with the token(s). + * @param user {object} The user associated with the token(s). + * @return {Promise} + * @fulfil {{accessToken:string,accessTokenExpiresAt:Date,refreshToken: string,refreshTokenExpiresAt: Date,scope: string[],client: ClientData,user: object}} An `Object` representing the token(s) and associated data. + * @example + * function saveToken(token, client, user) { + * // imaginary DB queries + * let fns = [ + * db.saveAccessToken({ + * access_token: token.accessToken, + * expires_at: token.accessTokenExpiresAt, + * scope: token.scope, + * client_id: client.id, + * user_id: user.id + * }), + * db.saveRefreshToken({ + * refresh_token: token.refreshToken, + * expires_at: token.refreshTokenExpiresAt, + * scope: token.scope, + * client_id: client.id, + * user_id: user.id + * }) + * ]; + * return Promise.all(fns); + * .spread(function(accessToken, refreshToken) { + * return { + * accessToken: accessToken.access_token, + * accessTokenExpiresAt: accessToken.expires_at, + * refreshToken: refreshToken.refresh_token, + * refreshTokenExpiresAt: refreshToken.expires_at, + * scope: accessToken.scope, + * client: {id: accessToken.client_id}, + * user: {id: accessToken.user_id} + * }; + * }); + * } + */ + async saveToken(token, client, user) { + throw new ServerError("saveToken not implemented"); + } - /*------------------------------------------------------------------------- + /*------------------------------------------------------------------------- | PARTIALLY REQUIRED *------------------------------------------------------------------------- | The following functions are required by specific grant types or under | specific conditions. */ - /** - * Invoked to retrieve a user using a username/password combination. - * This model function is **required** if the `password` grant is used. - * Please note, that password grant is considered unsafe. - * It is still supported but marked deprecated. - * - * **Invoked during:** - * - `password` grant - * - * @deprecated - * @async - * @param username {string} The username of the user to retrieve. - * @param password {string} The user's password. - * @param client {ClientData=} The client. - * @return {Promise} An `Object` representing the user, or a falsy value if no such user could be found. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. - * @example - * function getUser(username, password) { - * // imaginary DB query - * return db.queryUser({username: username, password: password}); - * } - */ - async getUser(username, password, client) { - throw new ServerError('getUser not implemented'); - } + /** + * Invoked to retrieve a user using a username/password combination. + * This model function is **required** if the `password` grant is used. + * Please note, that password grant is considered unsafe. + * It is still supported but marked deprecated. + * + * **Invoked during:** + * - `password` grant + * + * @deprecated + * @async + * @param username {string} The username of the user to retrieve. + * @param password {string} The user's password. + * @param client {ClientData=} The client. + * @return {Promise} An `Object` representing the user, or a falsy value if no such user could be found. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. + * @example + * function getUser(username, password) { + * // imaginary DB query + * return db.queryUser({username: username, password: password}); + * } + */ + async getUser(username, password, client) { + throw new ServerError("getUser not implemented"); + } - /** - * Invoked to retrieve the user associated with the specified client. - * This model function is **required** if the `client_credentials` grant is used. - * - * **Invoked during:** - * - `client_credentials` grant - * - * **Remarks:** - * - * `client` is the object previously obtained through `Model#getClient() `. - * - * @async - * @instance - * @param client {ClientData} The client to retrieve the associated user for. - * @return {Promise} An `Object` representing the user, or a falsy value if the client does not have an associated user. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. - * @example - * function getUserFromClient(client) { - * // imaginary DB query - * return db.queryUser({id: client.user_id}); - * } - */ - async getUserFromClient(client) { - throw new ServerError('getUserFromClient not implemented'); - } + /** + * Invoked to retrieve the user associated with the specified client. + * This model function is **required** if the `client_credentials` grant is used. + * + * **Invoked during:** + * - `client_credentials` grant + * + * **Remarks:** + * + * `client` is the object previously obtained through `Model#getClient() `. + * + * @async + * @instance + * @param client {ClientData} The client to retrieve the associated user for. + * @return {Promise} An `Object` representing the user, or a falsy value if the client does not have an associated user. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. + * @example + * function getUserFromClient(client) { + * // imaginary DB query + * return db.queryUser({id: client.user_id}); + * } + */ + async getUserFromClient(client) { + throw new ServerError("getUserFromClient not implemented"); + } - /** + /** * Invoked to retrieve an existing access token, including associated data, that has previously been saved through `Model#saveToken() `. * This model function is **required** if `OAuth2Server#authenticate() ` is used. * @@ -262,11 +261,11 @@ class Model { // eslint-disable-line no-unused-vars * }); * } */ - async getAccessToken(accessToken) { - throw new ServerError('getAccessToken not implemented'); - } + async getAccessToken(accessToken) { + throw new ServerError("getAccessToken not implemented"); + } - /** + /** * Invoked to retrieve an existing refresh token previously saved through `Model#saveToken() `. * This model function is **required** if the `refresh_token` grant is used. * **Invoked during:** @@ -299,11 +298,11 @@ class Model { // eslint-disable-line no-unused-vars * }); * } */ - async getRefreshToken(refreshToken) { - throw new ServerError('getRefreshToken not implemented'); - } + async getRefreshToken(refreshToken) { + throw new ServerError("getRefreshToken not implemented"); + } - /** + /** * Invoked to retrieve an existing authorization code previously saved through `Model#saveAuthorizationCode() `. * This model function is **required** if the `authorization_code` grant is used. * **Invoked during:** @@ -338,11 +337,11 @@ class Model { // eslint-disable-line no-unused-vars * }); * } */ - async getAuthorizationCode(authorizationCode) { - throw new ServerError('getAuthorizationCode not implemented'); - } + async getAuthorizationCode(authorizationCode) { + throw new ServerError("getAuthorizationCode not implemented"); + } - /** + /** * Invoked to save an authorization code. * This model function is **required** if the `authorization_code` grant is used. * @@ -385,11 +384,11 @@ class Model { // eslint-disable-line no-unused-vars * }); * } */ - async saveAuthorizationCode(code, client, user) { - throw new ServerError('saveAuthorizationCode not implemented'); - } + async saveAuthorizationCode(code, client, user) { + throw new ServerError("saveAuthorizationCode not implemented"); + } - /** + /** * Invoked to revoke a refresh token. * This model function is **required** if the `refresh_token` grant is used. * **Invoked during:** @@ -411,30 +410,30 @@ class Model { // eslint-disable-line no-unused-vars * }); * } */ - async revokeToken(token) { - throw new ServerError('revokeToken not implemented'); - } + async revokeToken(token) { + throw new ServerError("revokeToken not implemented"); + } - /** - * Invoked to revoke an authorization code. - * This model function is **required** if the `authorization_code` grant is used. - * - * **Invoked during:** - * - `authorization_code` grant - * - * **Remarks:** - * `code` is the authorization code object previously obtained through {@link Model#getAuthorizationCode}. - * - * @async - * @method - * @param code {AuthorizationCodeData} - * @return {Promise} Return `true` if the revocation was successful or `false` if the authorization code could not be found. - */ - async revokeAuthorizationCode(code) { - throw new ServerError('revokeAuthorizationCode not implemented'); - } + /** + * Invoked to revoke an authorization code. + * This model function is **required** if the `authorization_code` grant is used. + * + * **Invoked during:** + * - `authorization_code` grant + * + * **Remarks:** + * `code` is the authorization code object previously obtained through {@link Model#getAuthorizationCode}. + * + * @async + * @method + * @param code {AuthorizationCodeData} + * @return {Promise} Return `true` if the revocation was successful or `false` if the authorization code could not be found. + */ + async revokeAuthorizationCode(code) { + throw new ServerError("revokeAuthorizationCode not implemented"); + } - /** + /** * Invoked during request authentication to check if the provided access token was authorized the requested scopes. * * This model function is **required** if scopes are used with `OAuth2Server#authenticate() ` @@ -461,18 +460,17 @@ class Model { // eslint-disable-line no-unused-vars * return requestedScopes.every(s => authorizedScopes.includes(s)); * } */ - async verifyScope(accessToken, scope) { - throw new ServerError('verifyScope not implemented'); - } - + async verifyScope(accessToken, scope) { + throw new ServerError("verifyScope not implemented"); + } - /*------------------------------------------------------------------------- + /*------------------------------------------------------------------------- | OPTIONAL *------------------------------------------------------------------------- | The following functions are entirely optional */ - /** + /** * Invoked to generate a new access token. * This model function is **optional**. * @@ -496,11 +494,11 @@ class Model { // eslint-disable-line no-unused-vars * @param scope {string[]?} The scopes associated with the token. Can be `null` * @return {Promise.} A `String` to be used as access token. */ - async generateAccessToken(client, user, scope) { - throw new ServerError('generateAccessToken not implemented'); - } + async generateAccessToken(client, user, scope) { + throw new ServerError("generateAccessToken not implemented"); + } - /** + /** * Invoked to generate a new refresh token. * * This model function is **optional**. If not implemented, a default handler is used that generates refresh tokens consisting of 40 characters in the range of `a..z0..9`. @@ -525,11 +523,11 @@ class Model { // eslint-disable-line no-unused-vars * @param scope {string[]?} The scopes associated with the refresh token. Can be `null` * @return {Promise} A `String` to be used as refresh token. */ - async generateRefreshToken(client, user, scope) { - throw new ServerError('generateRefreshToken not implemented'); - } + async generateRefreshToken(client, user, scope) { + throw new ServerError("generateRefreshToken not implemented"); + } - /** + /** * Invoked to generate a new authorization code. * This model function is **optional**. If not implemented, a default handler is used that generates authorization codes consisting of 40 characters in the range of `a..z0..9`. * [RFC 6749 Appendix A.11](https://www.rfc-editor.org/6749#appendix-A.11) specifies that authorization codes must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters). @@ -546,11 +544,11 @@ class Model { // eslint-disable-line no-unused-vars * @return {Promise} A `String` to be used as authorization code. * */ - async generateAuthorizationCode(client, user, scope) { - throw new ServerError('generateAuthorizationCode not implemented'); - } + async generateAuthorizationCode(client, user, scope) { + throw new ServerError("generateAuthorizationCode not implemented"); + } - /** + /** * Invoked to check if the requested `scope` is valid for a particular `client`/`user` combination. * * This model function is **optional**. If not implemented, any scope is accepted. @@ -591,11 +589,11 @@ class Model { // eslint-disable-line no-unused-vars * return scope.filter(s => VALID_SCOPES.indexOf(s) >= 0); * } */ - async validateScope(user, client, scope) { - throw new ServerError('validateScope not implemented'); - } + async validateScope(user, client, scope) { + throw new ServerError("validateScope not implemented"); + } - /** + /** * Invoked to check if the provided `redirectUri` is valid for a particular `client`. * This model function is **optional**. If not implemented, the `redirectUri` should be included in the provided `redirectUris` of the client. * @@ -613,10 +611,9 @@ class Model { // eslint-disable-line no-unused-vars * @param client {object} The associated client. * @return {Promise} Returns `true` if the `redirectUri` is valid, `false` otherwise. */ - async validateRedirectUri(redirectUri, client) { - throw new ServerError('validateRedirectUri not implemented'); - } + async validateRedirectUri(redirectUri, client) { + throw new ServerError("validateRedirectUri not implemented"); + } } - -module.exports = Model; \ No newline at end of file +module.exports = Model; diff --git a/lib/models/token-model.js b/lib/models/token-model.js index ff24ae81..e235f4de 100644 --- a/lib/models/token-model.js +++ b/lib/models/token-model.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const { getLifetimeFromExpiresAt } = require('../utils/date-util'); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const { getLifetimeFromExpiresAt } = require("../utils/date-util"); /** * @private @@ -13,13 +13,13 @@ const { getLifetimeFromExpiresAt } = require('../utils/date-util'); * @description The core model attributes allowed when `allowExtendedTokenAttributes` is `false`. */ const modelAttributes = new Set([ - 'accessToken', - 'accessTokenExpiresAt', - 'refreshToken', - 'refreshTokenExpiresAt', - 'scope', - 'client', - 'user' + "accessToken", + "accessTokenExpiresAt", + "refreshToken", + "refreshTokenExpiresAt", + "scope", + "client", + "user", ]); /** @@ -27,66 +27,70 @@ const modelAttributes = new Set([ * @classDesc */ class TokenModel { - /** - * @constructor - * @param data - * @param options - */ - constructor(data = {}, options = {}) { - const { - accessToken, - accessTokenExpiresAt, - refreshToken, - refreshTokenExpiresAt, - scope, - client, - user, - } = data; + /** + * @constructor + * @param data + * @param options + */ + constructor(data = {}, options = {}) { + const { + accessToken, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope, + client, + user, + } = data; - if (!accessToken) { - throw new InvalidArgumentError('Missing parameter: `accessToken`'); - } + if (!accessToken) { + throw new InvalidArgumentError("Missing parameter: `accessToken`"); + } - if (!client) { - throw new InvalidArgumentError('Missing parameter: `client`'); - } + if (!client) { + throw new InvalidArgumentError("Missing parameter: `client`"); + } - if (!user) { - throw new InvalidArgumentError('Missing parameter: `user`'); - } + if (!user) { + throw new InvalidArgumentError("Missing parameter: `user`"); + } - if (accessTokenExpiresAt && !(accessTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError('Invalid parameter: `accessTokenExpiresAt`'); - } + if (accessTokenExpiresAt && !(accessTokenExpiresAt instanceof Date)) { + throw new InvalidArgumentError( + "Invalid parameter: `accessTokenExpiresAt`", + ); + } - if (refreshTokenExpiresAt && !(refreshTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError('Invalid parameter: `refreshTokenExpiresAt`'); - } + if (refreshTokenExpiresAt && !(refreshTokenExpiresAt instanceof Date)) { + throw new InvalidArgumentError( + "Invalid parameter: `refreshTokenExpiresAt`", + ); + } - this.accessToken = accessToken; - this.accessTokenExpiresAt = accessTokenExpiresAt; - this.client = client; - this.refreshToken = refreshToken; - this.refreshTokenExpiresAt = refreshTokenExpiresAt; - this.scope = scope; - this.user = user; + this.accessToken = accessToken; + this.accessTokenExpiresAt = accessTokenExpiresAt; + this.client = client; + this.refreshToken = refreshToken; + this.refreshTokenExpiresAt = refreshTokenExpiresAt; + this.scope = scope; + this.user = user; - if (accessTokenExpiresAt) { - this.accessTokenLifetime = getLifetimeFromExpiresAt(accessTokenExpiresAt); - } + if (accessTokenExpiresAt) { + this.accessTokenLifetime = getLifetimeFromExpiresAt(accessTokenExpiresAt); + } - const { allowExtendedTokenAttributes } = options; + const { allowExtendedTokenAttributes } = options; - if (allowExtendedTokenAttributes) { - this.customAttributes = {}; + if (allowExtendedTokenAttributes) { + this.customAttributes = {}; - Object.keys(data).forEach(key => { - if (!modelAttributes.has(key)) { - this.customAttributes[key] = data[key]; - } - }); - } - } + Object.keys(data).forEach((key) => { + if (!modelAttributes.has(key)) { + this.customAttributes[key] = data[key]; + } + }); + } + } } module.exports = TokenModel; diff --git a/lib/pkce/pkce.js b/lib/pkce/pkce.js index f72bdca5..c0b7cdaa 100644 --- a/lib/pkce/pkce.js +++ b/lib/pkce/pkce.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const { base64URLEncode } = require('../utils/string-util'); -const { createHash } = require('../utils/crypto-util'); +const { base64URLEncode } = require("../utils/string-util"); +const { createHash } = require("../utils/crypto-util"); /** * ABNF for "code_verifier" and "code_challenge" is as follows. @@ -15,13 +15,13 @@ const { createHash } = require('../utils/crypto-util'); * DIGIT = %x30-39 * @type {RegExp} */ -const codeChallengeAndVerifierRegexp = /^([\u0041-\u005a\u0061-\u007A0-9.\-_~]){43,128}$/; +const codeChallengeAndVerifierRegexp = + /^([\u0041-\u005a\u0061-\u007A0-9.\-_~]){43,128}$/; /** * @module pkce */ - /** * Return hash for code-challenge method-type. * @@ -31,18 +31,22 @@ const codeChallengeAndVerifierRegexp = /^([\u0041-\u005a\u0061-\u007A0-9.\-_~]){ * @return {String|undefined} */ function getHashForCodeChallenge({ method, verifier }) { - // to prevent undesired side-effects when passing some weird values - // to createHash or base64URLEncode we first check if the values are right - if (isValidMethod(method) && typeof verifier === 'string' && verifier.length > 0) { - if (method === 'plain') { - return verifier; - } + // to prevent undesired side-effects when passing some weird values + // to createHash or base64URLEncode we first check if the values are right + if ( + isValidMethod(method) && + typeof verifier === "string" && + verifier.length > 0 + ) { + if (method === "plain") { + return verifier; + } - if (method === 'S256') { - const hash = createHash({ data: verifier }); - return base64URLEncode(hash); - } - } + if (method === "S256") { + const hash = createHash({ data: verifier }); + return base64URLEncode(hash); + } + } } /** @@ -57,11 +61,13 @@ function getHashForCodeChallenge({ method, verifier }) { * @param codeChallenge {String} * @return {Boolean} */ -function codeChallengeMatchesABNF (codeChallenge) { - return typeof codeChallenge === 'string' && codeChallengeAndVerifierRegexp.test(codeChallenge); +function codeChallengeMatchesABNF(codeChallenge) { + return ( + typeof codeChallenge === "string" && + codeChallengeAndVerifierRegexp.test(codeChallenge) + ); } - /** * Check if the request is a PCKE request. We assume PKCE if grant type is * 'authorization_code' and code verifier is present. @@ -70,11 +76,10 @@ function codeChallengeMatchesABNF (codeChallenge) { * @param codeVerifier {String} * @return {boolean} */ -function isPKCERequest ({ grantType, codeVerifier }) { - return grantType === 'authorization_code' && !!codeVerifier; +function isPKCERequest({ grantType, codeVerifier }) { + return grantType === "authorization_code" && !!codeVerifier; } - /** * Checks if the code challenge method is one of the supported methods * 'sha256' or 'plain' @@ -82,13 +87,13 @@ function isPKCERequest ({ grantType, codeVerifier }) { * @param method {String} * @return {boolean} */ -function isValidMethod (method) { - return method === 'S256' || method === 'plain'; +function isValidMethod(method) { + return method === "S256" || method === "plain"; } module.exports = { - getHashForCodeChallenge, - codeChallengeMatchesABNF, - isPKCERequest, - isValidMethod + getHashForCodeChallenge, + codeChallengeMatchesABNF, + isPKCERequest, + isValidMethod, }; diff --git a/lib/request.js b/lib/request.js index f42960ca..7f4e6c9b 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,11 +1,11 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const InvalidArgumentError = require('./errors/invalid-argument-error'); +const InvalidArgumentError = require("./errors/invalid-argument-error"); /* type-is: https://github.com/jshttp/type-is */ -const typeis = require('type-is'); +const typeis = require("type-is"); /** * Wrapper for webserver's request. @@ -19,64 +19,64 @@ const typeis = require('type-is'); * } */ class Request { - /** - * Creates a new request instance - * @constructor - * @param headers {object} key-value object of headers - * @param method {string} the HTTP method - * @param query {object} key-value object of query parameters - * @param body {object=} optional key-value object of body parameters - * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver - * @throws {InvalidArgumentError} if one of headers, method or query are missing. - */ - constructor({ headers, method, query, body, ...otherOptions } = {}) { - if (!headers) { - throw new InvalidArgumentError('Missing parameter: `headers`'); - } + /** + * Creates a new request instance + * @constructor + * @param headers {object} key-value object of headers + * @param method {string} the HTTP method + * @param query {object} key-value object of query parameters + * @param body {object=} optional key-value object of body parameters + * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver + * @throws {InvalidArgumentError} if one of headers, method or query are missing. + */ + constructor({ headers, method, query, body, ...otherOptions } = {}) { + if (!headers) { + throw new InvalidArgumentError("Missing parameter: `headers`"); + } - if (!method) { - throw new InvalidArgumentError('Missing parameter: `method`'); - } + if (!method) { + throw new InvalidArgumentError("Missing parameter: `method`"); + } - if (!query) { - throw new InvalidArgumentError('Missing parameter: `query`'); - } + if (!query) { + throw new InvalidArgumentError("Missing parameter: `query`"); + } - this.body = body || {}; - this.headers = {}; - this.method = method; - this.query = query; + this.body = body || {}; + this.headers = {}; + this.method = method; + this.query = query; - // Store the headers in lower case. - Object.entries(headers).forEach(([header, value]) => { - this.headers[header.toLowerCase()] = value; - }); + // Store the headers in lower case. + Object.entries(headers).forEach(([header, value]) => { + this.headers[header.toLowerCase()] = value; + }); - // Store additional properties of the request object passed in - Object.entries(otherOptions) - .filter(([property]) => !this[property]) - .forEach(([property, value]) => { - this[property] = value; - }); - } + // Store additional properties of the request object passed in + Object.entries(otherOptions) + .filter(([property]) => !this[property]) + .forEach(([property, value]) => { + this[property] = value; + }); + } - /** - * Get a request header (case-insensitive). - * @param {String} field - * @return {string} - */ - get(field) { - return this.headers[field.toLowerCase()]; - } + /** + * Get a request header (case-insensitive). + * @param {String} field + * @return {string} + */ + get(field) { + return this.headers[field.toLowerCase()]; + } - /** - * Check if the content-type matches any of the given mime types. - * @param {...string[]} types - * @return {boolean} - */ - is(...types) { - return typeis(this, types.flat()) || false; - } + /** + * Check if the content-type matches any of the given mime types. + * @param {...string[]} types + * @return {boolean} + */ + is(...types) { + return typeis(this, types.flat()) || false; + } } module.exports = Request; diff --git a/lib/response-types/code-response-type.js b/lib/response-types/code-response-type.js index ccafdd22..0198bd59 100644 --- a/lib/response-types/code-response-type.js +++ b/lib/response-types/code-response-type.js @@ -1,47 +1,47 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const InvalidArgumentError = require('../errors/invalid-argument-error'); -const url = require('url'); +const InvalidArgumentError = require("../errors/invalid-argument-error"); +const url = require("url"); /** * @class * @classDesc */ class CodeResponseType { - /** - * @constructor - * @param code - * @throws {InvalidArgumentError} if {code} is missing - */ - constructor(code) { - if (!code) { - throw new InvalidArgumentError('Missing parameter: `code`'); - } - - this.code = code; - } - - /** - * @param redirectUri - * @return {UrlWithParsedQuery} - * @throws {InvalidArgumentError} if redirectUri is missing - */ - buildRedirectUri(redirectUri) { - if (!redirectUri) { - throw new InvalidArgumentError('Missing parameter: `redirectUri`'); - } - - const uri = url.parse(redirectUri, true); - - uri.query.code = this.code; - uri.search = null; - - return uri; - } + /** + * @constructor + * @param code + * @throws {InvalidArgumentError} if {code} is missing + */ + constructor(code) { + if (!code) { + throw new InvalidArgumentError("Missing parameter: `code`"); + } + + this.code = code; + } + + /** + * @param redirectUri + * @return {UrlWithParsedQuery} + * @throws {InvalidArgumentError} if redirectUri is missing + */ + buildRedirectUri(redirectUri) { + if (!redirectUri) { + throw new InvalidArgumentError("Missing parameter: `redirectUri`"); + } + + const uri = url.parse(redirectUri, true); + + uri.query.code = this.code; + uri.search = null; + + return uri; + } } module.exports = CodeResponseType; diff --git a/lib/response-types/token-response-type.js b/lib/response-types/token-response-type.js index e06f9fc3..f2ce2f99 100644 --- a/lib/response-types/token-response-type.js +++ b/lib/response-types/token-response-type.js @@ -1,23 +1,23 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const ServerError = require('../errors/server-error'); +const ServerError = require("../errors/server-error"); /** * @class * @classDesc */ class TokenResponseType { - /** - * @constructor - * @throws {ServerError} not implemented yet - */ - constructor() { - throw new ServerError('Not implemented.'); - } + /** + * @constructor + * @throws {ServerError} not implemented yet + */ + constructor() { + throw new ServerError("Not implemented."); + } } module.exports = TokenResponseType; diff --git a/lib/response.js b/lib/response.js index 5afec5c8..8d8847d4 100644 --- a/lib/response.js +++ b/lib/response.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * Wrapper for webserver's response object. @@ -12,59 +12,58 @@ * } */ class Response { + /** + * Create a new Response instance. + * @constructor + * @param headers {object} key-value object of headers + * @param method {string} the HTTP method + * @param body {object=} optional key-value object of body parameters + * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver + */ + constructor({ headers = {}, body = {}, ...otherOptions } = {}) { + this.status = 200; + this.body = body; + this.headers = {}; - /** - * Create a new Response instance. - * @constructor - * @param headers {object} key-value object of headers - * @param method {string} the HTTP method - * @param body {object=} optional key-value object of body parameters - * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver - */ - constructor({ headers = {}, body = {}, ...otherOptions } = {}) { - this.status = 200; - this.body = body; - this.headers = {}; + // Store the headers in lower case. + Object.entries(headers).forEach(([header, value]) => { + this.headers[header.toLowerCase()] = value; + }); - // Store the headers in lower case. - Object.entries(headers).forEach(([header, value]) => { - this.headers[header.toLowerCase()] = value; - }); + // Store additional properties of the response object passed in + Object.entries(otherOptions) + .filter(([property]) => !this[property]) + .forEach(([property, value]) => { + this[property] = value; + }); + } - // Store additional properties of the response object passed in - Object.entries(otherOptions) - .filter(([property]) => !this[property]) - .forEach(([property, value]) => { - this[property] = value; - }); - } + /** + * Get a response header. + * @param field {string} the field to access, case-insensitive + * @return {string|undefined} + */ + get(field) { + return this.headers[field.toLowerCase()]; + } - /** - * Get a response header. - * @param field {string} the field to access, case-insensitive - * @return {string|undefined} - */ - get(field) { - return this.headers[field.toLowerCase()]; - } + /** + * Redirect response. + * @param url {string} the url to redirect to + */ + redirect(url) { + this.set("Location", url); + this.status = 302; + } - /** - * Redirect response. - * @param url {string} the url to redirect to - */ - redirect(url) { - this.set('Location', url); - this.status = 302; - } - - /** - * Set a response header. - * @param field {string} the name of the header field, case-insensitive - * @param value {string} the new value of the header field - */ - set(field, value) { - this.headers[field.toLowerCase()] = value; - } + /** + * Set a response header. + * @param field {string} the name of the header field, case-insensitive + * @param value {string} the new value of the header field + */ + set(field, value) { + this.headers[field.toLowerCase()] = value; + } } module.exports = Response; diff --git a/lib/server.js b/lib/server.js index 320ddea7..b5800434 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,14 +1,14 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const AuthenticateHandler = require('./handlers/authenticate-handler'); -const AuthorizeHandler = require('./handlers/authorize-handler'); -const InvalidArgumentError = require('./errors/invalid-argument-error'); -const TokenHandler = require('./handlers/token-handler'); +const AuthenticateHandler = require("./handlers/authenticate-handler"); +const AuthorizeHandler = require("./handlers/authorize-handler"); +const InvalidArgumentError = require("./errors/invalid-argument-error"); +const TokenHandler = require("./handlers/token-handler"); // we require the model only for JSDoc linking -require('./model'); +require("./model"); /** * @class @@ -17,223 +17,234 @@ require('./model'); * const OAuth2Server = require('@node-oauth/oauth2-server'); */ class OAuth2Server { + /** + * Instantiates `OAuth2Server` using the supplied model. + * **Remarks:** + * - Any valid option for {@link OAuth2Server#authenticate}, {@link OAuth2Server#authorize} and {@link OAuth2Server#token} can be passed to the constructor as well. + * - The supplied options will be used as default for the other methods. + * + * @constructor + * @param options {object} Server options. + * @param options.model {Model} The Model; this is always required. + * + * @param options.scope {string[]|undefined} The scope(s) to authenticate. + * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. + * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. + * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. + * + * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). + * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user + * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state + * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) + * + * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). + * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). + * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). + * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. + * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. + * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. + * @param [options.enablePlainPKCE=false] {boolean} Allow the use of the `plain` code challenge method for PKCE. This is not recommended for production environments. + * + * @throws {InvalidArgumentError} if the model is missing + * @return {OAuth2Server} A new `OAuth2Server` instance. + * @example + * // Basic usage: + * const oauth = new OAuth2Server({ + * model: require('./model') + * }); + * @example + * // Advanced example with additional options: + * const oauth = new OAuth2Server({ + * model: require('./model'), + * allowBearerTokensInQueryString: true, + * accessTokenLifetime: 4 * 60 * 60 + * }); + */ + constructor(options) { + options = options || {}; - /** - * Instantiates `OAuth2Server` using the supplied model. - * **Remarks:** - * - Any valid option for {@link OAuth2Server#authenticate}, {@link OAuth2Server#authorize} and {@link OAuth2Server#token} can be passed to the constructor as well. - * - The supplied options will be used as default for the other methods. - * - * @constructor - * @param options {object} Server options. - * @param options.model {Model} The Model; this is always required. - * - * @param options.scope {string[]|undefined} The scope(s) to authenticate. - * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. - * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. - * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. - * - * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). - * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user - * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state - * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) - * - * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). - * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). - * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). - * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. - * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. - * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. - * @param [options.enablePlainPKCE=false] {boolean} Allow the use of the `plain` code challenge method for PKCE. This is not recommended for production environments. - * - * @throws {InvalidArgumentError} if the model is missing - * @return {OAuth2Server} A new `OAuth2Server` instance. - * @example - * // Basic usage: - * const oauth = new OAuth2Server({ - * model: require('./model') - * }); - * @example - * // Advanced example with additional options: - * const oauth = new OAuth2Server({ - * model: require('./model'), - * allowBearerTokensInQueryString: true, - * accessTokenLifetime: 4 * 60 * 60 - * }); - */ - constructor (options) { - options = options || {}; + if (!options.model) { + throw new InvalidArgumentError("Missing parameter: `model`"); + } - if (!options.model) { - throw new InvalidArgumentError('Missing parameter: `model`'); - } + this.options = options; + } - this.options = options; - } + /** + * Authenticates a request. + * @function + * @param options.scope {string[]|undefined} The scope(s) to authenticate. + * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. + * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. + * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. + * @throws {UnauthorizedRequestError} The protected resource request failed authentication. + * @return {Promise.} A `Promise` that resolves to the access token object returned from the model's `getAccessToken`. + * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. + * @example + * const oauth = new OAuth2Server({model: ...}); + * function authenticateHandler(options) { + * return function(req, res, next) { + * let request = new Request(req); + * let response = new Response(res); + * return oauth.authenticate(request, response, options) + * .then(function(token) { + * res.locals.oauth = {token: token}; + * next(); + * }) + * .catch(function(err) { + * // handle error condition + * }); + * } + * } + */ + authenticate(request, response, options) { + options = Object.assign( + { + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + allowBearerTokensInQueryString: false, + }, + this.options, + options, + ); - /** - * Authenticates a request. - * @function - * @param options.scope {string[]|undefined} The scope(s) to authenticate. - * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. - * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. - * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. - * @throws {UnauthorizedRequestError} The protected resource request failed authentication. - * @return {Promise.} A `Promise` that resolves to the access token object returned from the model's `getAccessToken`. - * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. - * @example - * const oauth = new OAuth2Server({model: ...}); - * function authenticateHandler(options) { - * return function(req, res, next) { - * let request = new Request(req); - * let response = new Response(res); - * return oauth.authenticate(request, response, options) - * .then(function(token) { - * res.locals.oauth = {token: token}; - * next(); - * }) - * .catch(function(err) { - * // handle error condition - * }); - * } - * } - */ - authenticate (request, response, options) { - options = Object.assign({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - allowBearerTokensInQueryString: false - }, this.options, options); + return new AuthenticateHandler(options).handle(request, response); + } - return new AuthenticateHandler(options).handle(request, response); - } + /** + * Authorizes a token request. + * **Remarks:** + * + * If `request.query.allowed` equals the string `'false'` the access request is denied and the returned promise is rejected with an `AccessDeniedError`. + * + * In order to retrieve the user associated with the request, `options.authenticateHandler` should be supplied. + * The `authenticateHandler` has to be an object implementing a `handle(request, response)` function that returns a user object. + * If there is no associated user (i.e. the user is not logged in) a falsy value should be returned. + * + * ```js + * let authenticateHandler = { + * handle: function(request, response) { + * return // get authenticated user; + * } + * }; + * ``` + * When working with a session-based login mechanism, the handler can simply look like this: + * ```js + * let authenticateHandler = { + * handle: function(request, response) { + * return request.session.user; + * } + * }; + * ``` + * + * @function + * @param request {Request} the Request instance object + * @param request.query.allowed {string=} `'false'` to deny the authorization request (see remarks section). + * @param response {Response} the Response instance object + * @param options {object=} handler options + * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). + * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user + * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state + * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) + * @throws {AccessDeniedError} The resource owner denied the access request (i.e. `request.query.allow` was `'false'`). + * @return {Promise.} A `Promise` that resolves to the authorization code object returned from model's `saveAuthorizationCode` + * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. + * @example + * const oauth = new OAuth2Server({model: ...}); + * function authorizeHandler(options) { + * return function(req, res, next) { + * let request = new Request(req); + * let response = new Response(res); + * return oauth.authorize(request, response, options) + * .then(function(code) { + * res.locals.oauth = {code: code}; + * next(); + * }) + * .catch(function(err) { + * // handle error condition + * }); + * } + * } + */ + authorize(request, response, options) { + options = Object.assign( + { + allowEmptyState: false, + authorizationCodeLifetime: 5 * 60, // 5 minutes. + }, + this.options, + options, + ); - /** - * Authorizes a token request. - * **Remarks:** - * - * If `request.query.allowed` equals the string `'false'` the access request is denied and the returned promise is rejected with an `AccessDeniedError`. - * - * In order to retrieve the user associated with the request, `options.authenticateHandler` should be supplied. - * The `authenticateHandler` has to be an object implementing a `handle(request, response)` function that returns a user object. - * If there is no associated user (i.e. the user is not logged in) a falsy value should be returned. - * - * ```js - * let authenticateHandler = { - * handle: function(request, response) { - * return // get authenticated user; - * } - * }; - * ``` - * When working with a session-based login mechanism, the handler can simply look like this: - * ```js - * let authenticateHandler = { - * handle: function(request, response) { - * return request.session.user; - * } - * }; - * ``` - * - * @function - * @param request {Request} the Request instance object - * @param request.query.allowed {string=} `'false'` to deny the authorization request (see remarks section). - * @param response {Response} the Response instance object - * @param options {object=} handler options - * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). - * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user - * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state - * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) - * @throws {AccessDeniedError} The resource owner denied the access request (i.e. `request.query.allow` was `'false'`). - * @return {Promise.} A `Promise` that resolves to the authorization code object returned from model's `saveAuthorizationCode` - * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. - * @example - * const oauth = new OAuth2Server({model: ...}); - * function authorizeHandler(options) { - * return function(req, res, next) { - * let request = new Request(req); - * let response = new Response(res); - * return oauth.authorize(request, response, options) - * .then(function(code) { - * res.locals.oauth = {code: code}; - * next(); - * }) - * .catch(function(err) { - * // handle error condition - * }); - * } - * } - */ - authorize (request, response, options) { - options = Object.assign({ - allowEmptyState: false, - authorizationCodeLifetime: 5 * 60 // 5 minutes. - }, this.options, options); + return new AuthorizeHandler(options).handle(request, response); + } - return new AuthorizeHandler(options).handle(request, response); - } + /** + * Retrieves a new token for an authorized token request. + * **Remarks:** + * If `options.allowExtendedTokenAttributes` is `true` any additional properties set on the object returned from `Model#saveToken() ` are copied to the token response sent to the client. + * By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. + * ```js + * let options = { + * // ... + * // Allow token requests using the password grant to not include a client_secret. + * requireClientAuthentication: {password: false} + * }; + * ``` + * `options.extendedGrantTypes` is an object mapping extension grant URIs to handler types, for example: + * ```js + * let options = { + * // ... + * extendedGrantTypes: { + * 'urn:foo:bar:baz': MyGrantType + * } + * }; + * ``` + * For information on how to implement a handler for a custom grant type see the extension grants. + * @function + * @param request {Request} the Request instance object + * @param response {Response} the Response instance object + * @param options {object=} handler options + * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). + * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). + * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). + * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. + * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. + * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. + * @return {Promise.} A `Promise` that resolves to the token object returned from the model's `saveToken` method. + * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. + * @throws {InvalidGrantError} The access token request was invalid or not authorized. + * @example + * const oauth = new OAuth2Server({model: ...}); + * function tokenHandler(options) { + * return function(req, res, next) { + * let request = new Request(req); + * let response = new Response(res); + * return oauth.token(request, response, options) + * .then(function(code) { + * res.locals.oauth = {token: token}; + * next(); + * }) + * .catch(function(err) { + * // handle error condition + * }); + * } + * } + */ + token(request, response, options) { + options = Object.assign( + { + accessTokenLifetime: 60 * 60, // 1 hour. + refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks. + allowExtendedTokenAttributes: false, + requireClientAuthentication: {}, // defaults to true for all grant types + }, + this.options, + options, + ); - /** - * Retrieves a new token for an authorized token request. - * **Remarks:** - * If `options.allowExtendedTokenAttributes` is `true` any additional properties set on the object returned from `Model#saveToken() ` are copied to the token response sent to the client. - * By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. - * ```js - * let options = { - * // ... - * // Allow token requests using the password grant to not include a client_secret. - * requireClientAuthentication: {password: false} - * }; - * ``` - * `options.extendedGrantTypes` is an object mapping extension grant URIs to handler types, for example: - * ```js - * let options = { - * // ... - * extendedGrantTypes: { - * 'urn:foo:bar:baz': MyGrantType - * } - * }; - * ``` - * For information on how to implement a handler for a custom grant type see the extension grants. - * @function - * @param request {Request} the Request instance object - * @param response {Response} the Response instance object - * @param options {object=} handler options - * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). - * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). - * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). - * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. - * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. - * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. - * @return {Promise.} A `Promise` that resolves to the token object returned from the model's `saveToken` method. - * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. - * @throws {InvalidGrantError} The access token request was invalid or not authorized. - * @example - * const oauth = new OAuth2Server({model: ...}); - * function tokenHandler(options) { - * return function(req, res, next) { - * let request = new Request(req); - * let response = new Response(res); - * return oauth.token(request, response, options) - * .then(function(code) { - * res.locals.oauth = {token: token}; - * next(); - * }) - * .catch(function(err) { - * // handle error condition - * }); - * } - * } - */ - token (request, response, options) { - options = Object.assign({ - accessTokenLifetime: 60 * 60, // 1 hour. - refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks. - allowExtendedTokenAttributes: false, - requireClientAuthentication: {} // defaults to true for all grant types - }, this.options, options); - - return new TokenHandler(options).handle(request, response); - } + return new TokenHandler(options).handle(request, response); + } } module.exports = OAuth2Server; diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js index 373d8731..f76fde37 100644 --- a/lib/token-types/bearer-token-type.js +++ b/lib/token-types/bearer-token-type.js @@ -1,69 +1,74 @@ -'use strict'; +"use strict"; /* * Module dependencies. */ -const InvalidArgumentError = require('../errors/invalid-argument-error'); +const InvalidArgumentError = require("../errors/invalid-argument-error"); /** * @class * @classDesc */ class BearerTokenType { + /** + * @constructor + * @param accessToken + * @param accessTokenLifetime + * @param refreshToken + * @param scope + * @param customAttributes + */ + constructor( + accessToken, + accessTokenLifetime, + refreshToken, + scope, + customAttributes, + ) { + if (!accessToken) { + throw new InvalidArgumentError("Missing parameter: `accessToken`"); + } - /** - * @constructor - * @param accessToken - * @param accessTokenLifetime - * @param refreshToken - * @param scope - * @param customAttributes - */ - constructor(accessToken, accessTokenLifetime, refreshToken, scope, customAttributes) { - if (!accessToken) { - throw new InvalidArgumentError('Missing parameter: `accessToken`'); - } + this.accessToken = accessToken; + this.accessTokenLifetime = accessTokenLifetime; + this.refreshToken = refreshToken; + this.scope = scope; - this.accessToken = accessToken; - this.accessTokenLifetime = accessTokenLifetime; - this.refreshToken = refreshToken; - this.scope = scope; + if (customAttributes) { + this.customAttributes = customAttributes; + } + } - if (customAttributes) { - this.customAttributes = customAttributes; - } - } + /** + * Retrieve the value representation. + */ - /** - * Retrieve the value representation. - */ - - valueOf () { - const object = { - access_token: this.accessToken, - token_type: 'Bearer' - }; + valueOf() { + const object = { + access_token: this.accessToken, + token_type: "Bearer", + }; - if (this.accessTokenLifetime) { - object.expires_in = this.accessTokenLifetime; - } + if (this.accessTokenLifetime) { + object.expires_in = this.accessTokenLifetime; + } - if (this.refreshToken) { - object.refresh_token = this.refreshToken; - } + if (this.refreshToken) { + object.refresh_token = this.refreshToken; + } - if (this.scope) { - object.scope = this.scope; - } + if (this.scope) { + object.scope = this.scope; + } - for (const key in this.customAttributes) { - if ( Object.prototype.hasOwnProperty.call(this.customAttributes, key) ) { - object[key] = this.customAttributes[key]; - } - } - return object; - } + for (const key in this.customAttributes) { + if (Object.prototype.hasOwnProperty.call(this.customAttributes, key)) { + object[key] = this.customAttributes[key]; + } + } + return object; + } } module.exports = BearerTokenType; diff --git a/lib/token-types/mac-token-type.js b/lib/token-types/mac-token-type.js index 02a90114..44274aa4 100644 --- a/lib/token-types/mac-token-type.js +++ b/lib/token-types/mac-token-type.js @@ -1,19 +1,19 @@ -'use strict'; +"use strict"; -const ServerError = require('../errors/server-error'); +const ServerError = require("../errors/server-error"); /** * @class * @classDesc */ class MacTokenType { - /** - * @constructor - * @throws {ServerError} not yet implemented - */ - constructor() { - throw new ServerError('Not implemented.'); - } + /** + * @constructor + * @throws {ServerError} not yet implemented + */ + constructor() { + throw new ServerError("Not implemented."); + } } module.exports = MacTokenType; diff --git a/lib/utils/crypto-util.js b/lib/utils/crypto-util.js index d19942cc..4780c81d 100644 --- a/lib/utils/crypto-util.js +++ b/lib/utils/crypto-util.js @@ -1,6 +1,6 @@ -'use strict'; +"use strict"; -const crypto = require('crypto'); +const crypto = require("crypto"); /** * @module CryptoUtil @@ -17,11 +17,8 @@ const crypto = require('crypto'); * @param output {'base64'|'base64url'|'binary'|'hex'|undefined} optional, the desired output type * @return {Buffer|string} if {output} is undefined, a {Buffer} is returned, otherwise a {String} */ -const createHash = function({ algorithm = 'sha256', data, output, encoding }) { - return crypto - .createHash(algorithm) - .update(data, encoding) - .digest(output); +const createHash = function ({ algorithm = "sha256", data, output, encoding }) { + return crypto.createHash(algorithm).update(data, encoding).digest(output); }; module.exports = { createHash }; diff --git a/lib/utils/date-util.js b/lib/utils/date-util.js index 4fe32f97..ae8983c7 100644 --- a/lib/utils/date-util.js +++ b/lib/utils/date-util.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * @module DateUtil @@ -12,9 +12,9 @@ * @return {number} The number of seconds until the expiration date. */ function getLifetimeFromExpiresAt(expiresAt) { - return Math.floor((expiresAt - new Date()) / 1000); + return Math.floor((expiresAt - new Date()) / 1000); } module.exports = { - getLifetimeFromExpiresAt, + getLifetimeFromExpiresAt, }; diff --git a/lib/utils/scope-util.js b/lib/utils/scope-util.js index 5ed4bbd3..4c01cda1 100644 --- a/lib/utils/scope-util.js +++ b/lib/utils/scope-util.js @@ -1,5 +1,5 @@ -const isFormat = require('@node-oauth/formats'); -const InvalidScopeError = require('../errors/invalid-scope-error'); +const isFormat = require("@node-oauth/formats"); +const InvalidScopeError = require("../errors/invalid-scope-error"); const whiteSpace = /\s+/g; /** @@ -17,24 +17,24 @@ const whiteSpace = /\s+/g; * @return {undefined|string[]} * @see {https://github.com/node-oauth/formats} */ -function parseScope (requestedScope) { - if (requestedScope == null) { - return undefined; - } +function parseScope(requestedScope) { + if (requestedScope == null) { + return undefined; + } - if (typeof requestedScope !== 'string') { - throw new InvalidScopeError('Invalid parameter: `scope`'); - } + if (typeof requestedScope !== "string") { + throw new InvalidScopeError("Invalid parameter: `scope`"); + } - // XXX: this prevents spaced-only strings to become - // treated as valid nqchar by making them empty strings - requestedScope = requestedScope.trim(); + // XXX: this prevents spaced-only strings to become + // treated as valid nqchar by making them empty strings + requestedScope = requestedScope.trim(); - if(!isFormat.nqschar(requestedScope)) { - throw new InvalidScopeError('Invalid parameter: `scope`'); - } + if (!isFormat.nqschar(requestedScope)) { + throw new InvalidScopeError("Invalid parameter: `scope`"); + } - return requestedScope.split(whiteSpace); + return requestedScope.split(whiteSpace); } module.exports = { parseScope }; diff --git a/lib/utils/string-util.js b/lib/utils/string-util.js index 2835b6d6..7dad0e9d 100644 --- a/lib/utils/string-util.js +++ b/lib/utils/string-util.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * @module StringUtil @@ -11,10 +11,11 @@ * @return {string} */ function base64URLEncode(str) { - return str.toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); + return str + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); } module.exports = { base64URLEncode }; diff --git a/lib/utils/token-util.js b/lib/utils/token-util.js index 5c9e2392..a2f793ac 100644 --- a/lib/utils/token-util.js +++ b/lib/utils/token-util.js @@ -1,6 +1,6 @@ -'use strict'; +"use strict"; -const randomBytes = require('crypto').randomBytes; +const randomBytes = require("crypto").randomBytes; /** * @module TokenUtil @@ -13,17 +13,17 @@ const randomBytes = require('crypto').randomBytes; * @return {Promise} */ function generateRandomToken() { - return new Promise((resolve, reject) => { - randomBytes(32, (err, data) => { - if (err) { - reject(err); - } else { - resolve(data.toString('hex')); - } - }); - }); + return new Promise((resolve, reject) => { + randomBytes(32, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data.toString("hex")); + } + }); + }); } module.exports = { - generateRandomToken + generateRandomToken, }; diff --git a/test/assertions.js b/test/assertions.js index 7a95c809..0d9691b8 100644 --- a/test/assertions.js +++ b/test/assertions.js @@ -1,18 +1,18 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const chai = require('chai'); +const chai = require("chai"); /** * SHA-256 assertion. */ chai.use(function (_chai, utils) { - chai.Assertion.addMethod('sha256', function (...args) { - const obj = utils.flag(this, 'object'); - new chai.Assertion(obj).match(/^[a-f0-9]{64}$/i); - }); + chai.Assertion.addMethod("sha256", function (...args) { + const obj = utils.flag(this, "object"); + new chai.Assertion(obj).match(/^[a-f0-9]{64}$/i); + }); }); diff --git a/test/compliance/client-authentication_test.js b/test/compliance/client-authentication_test.js index 72624ec5..8750637d 100644 --- a/test/compliance/client-authentication_test.js +++ b/test/compliance/client-authentication_test.js @@ -18,111 +18,117 @@ * parameter if the client secret is an empty string. */ -const OAuth2Server = require('../..'); -const DB = require('../helpers/db'); -const createModel = require('../helpers/model'); -const createRequest = require('../helpers/request'); -const Response = require('../../lib/response'); +const OAuth2Server = require("../.."); +const DB = require("../helpers/db"); +const createModel = require("../helpers/model"); +const createRequest = require("../helpers/request"); +const Response = require("../../lib/response"); -require('chai').should(); +require("chai").should(); const db = new DB(); const auth = new OAuth2Server({ - model: createModel(db) + model: createModel(db), }); -const user = db.saveUser({ id: 1, username: 'test', password: 'test'}); -const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password'] }); -const scope = 'read write'; - -function createDefaultRequest () { - return createRequest({ - body: { - grant_type: 'password', - username: user.username, - password: user.password, - scope - }, - headers: { - 'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded' - }, - method: 'POST', - }); +const user = db.saveUser({ id: 1, username: "test", password: "test" }); +const client = db.saveClient({ id: "a", secret: "b", grants: ["password"] }); +const scope = "read write"; + +function createDefaultRequest() { + return createRequest({ + body: { + grant_type: "password", + username: user.username, + password: user.password, + scope, + }, + headers: { + authorization: + "Basic " + + Buffer.from(client.id + ":" + client.secret).toString("base64"), + "content-type": "application/x-www-form-urlencoded", + }, + method: "POST", + }); } -describe('Client Authentication Compliance', function () { - describe('No authentication', function () { - it('should be an unsuccesfull authentication', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.headers.authorization; - - await auth.token(request, response, {}) - .then((token) => { - throw new Error('Should not be here'); - }). - catch(err => { - err.name.should.equal('invalid_client'); - }); - }); - }); - - describe('Basic Authentication', function () { - it('should be a succesfull authentication', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - await auth.token(request, response, {}); - }); - - it('should be an unsuccesfull authentication', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - request.headers.authorization = 'Basic ' + Buffer.from('a:c').toString('base64'); - - await auth.token(request, response, {}) - .then((token) => { - throw new Error('Should not be here'); - }). - catch(err => { - err.name.should.equal('invalid_client'); - }); - }); - }); - - describe('Request body authentication', function () { - it('should be a succesfull authentication', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.headers.authorization; - - request.body.client_id = client.id; - request.body.client_secret = client.secret; - - await auth.token(request, response, {}); - }); - - it('should be an unsuccesfull authentication', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.headers.authorization; - - request.body.client_id = 'a'; - request.body.client_secret = 'c'; - - await auth.token(request, response, {}) - .then((token) => { - throw new Error('Should not be here'); - }) - .catch(err => { - err.name.should.equal('invalid_client'); - }); - }); - }); +describe("Client Authentication Compliance", function () { + describe("No authentication", function () { + it("should be an unsuccesfull authentication", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.headers.authorization; + + await auth + .token(request, response, {}) + .then((token) => { + throw new Error("Should not be here"); + }) + .catch((err) => { + err.name.should.equal("invalid_client"); + }); + }); + }); + + describe("Basic Authentication", function () { + it("should be a succesfull authentication", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + await auth.token(request, response, {}); + }); + + it("should be an unsuccesfull authentication", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.headers.authorization = + "Basic " + Buffer.from("a:c").toString("base64"); + + await auth + .token(request, response, {}) + .then((token) => { + throw new Error("Should not be here"); + }) + .catch((err) => { + err.name.should.equal("invalid_client"); + }); + }); + }); + + describe("Request body authentication", function () { + it("should be a succesfull authentication", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.headers.authorization; + + request.body.client_id = client.id; + request.body.client_secret = client.secret; + + await auth.token(request, response, {}); + }); + + it("should be an unsuccesfull authentication", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.headers.authorization; + + request.body.client_id = "a"; + request.body.client_secret = "c"; + + await auth + .token(request, response, {}) + .then((token) => { + throw new Error("Should not be here"); + }) + .catch((err) => { + err.name.should.equal("invalid_client"); + }); + }); + }); }); diff --git a/test/compliance/client-credential-workflow_test.js b/test/compliance/client-credential-workflow_test.js index 6c63c255..080ab412 100644 --- a/test/compliance/client-credential-workflow_test.js +++ b/test/compliance/client-credential-workflow_test.js @@ -14,129 +14,136 @@ * @see https://www.rfc-editor.org/rfc/rfc6749#section-4.4 */ -const OAuth2Server = require('../..'); -const DB = require('../helpers/db'); -const createModel = require('../helpers/model'); -const createRequest = require('../helpers/request'); -const Response = require('../../lib/response'); +const OAuth2Server = require("../.."); +const DB = require("../helpers/db"); +const createModel = require("../helpers/model"); +const createRequest = require("../helpers/request"); +const Response = require("../../lib/response"); -require('chai').should(); +require("chai").should(); const db = new DB(); // this user represents requests in the name of an external server // TODO: we should discuss, if we can make user optional for client credential workflows // as it's not desired to have an extra fake-user representing a server just to pass validation -const userDoc = { id: 'machine2-123456789', name: 'machine2' }; +const userDoc = { id: "machine2-123456789", name: "machine2" }; db.saveUser(userDoc); const oAuth2Server = new OAuth2Server({ - model: { - ...createModel(db), - getUserFromClient: async function (_client) { - // in a machine2machine setup we might not have a dedicated "user" - // but we need to return a truthy response to - const client = db.findClient(_client.id, _client.secret); - return client && { ...userDoc }; - } - } + model: { + ...createModel(db), + getUserFromClient: async function (_client) { + // in a machine2machine setup we might not have a dedicated "user" + // but we need to return a truthy response to + const client = db.findClient(_client.id, _client.secret); + return client && { ...userDoc }; + }, + }, }); const clientDoc = db.saveClient({ - id: 'client-credential-test-client', - secret: 'client-credential-test-secret', - grants: ['client_credentials'] + id: "client-credential-test-client", + secret: "client-credential-test-secret", + grants: ["client_credentials"], }); -const enabledScope = 'read write'; +const enabledScope = "read write"; -describe('ClientCredentials Workflow Compliance (4.4)', function () { - describe('Access Token Request (4.4.1)', function () { - /** - * 4.4.2. Access Token Request - * - * The client makes a request to the token endpoint by adding the - * following parameters using the "application/x-www-form-urlencoded" - * format per Appendix B with a character encoding of UTF-8 in the HTTP - * request entity-body: - * - * grant_type - * REQUIRED. Value MUST be set to "client_credentials". - * - * scope - * OPTIONAL. The scope of the access request as described by - * Section 3.3. - * - * The client MUST authenticate with the authorization server as - * described in Section 3.2.1. - */ - it('authenticates the client with valid credentials', async function () { - const response = new Response(); - const request = createRequest({ - body: { - grant_type: 'client_credentials', - scope: enabledScope - }, - headers: { - 'authorization': 'Basic ' + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded' - }, - method: 'POST', - }); +describe("ClientCredentials Workflow Compliance (4.4)", function () { + describe("Access Token Request (4.4.1)", function () { + /** + * 4.4.2. Access Token Request + * + * The client makes a request to the token endpoint by adding the + * following parameters using the "application/x-www-form-urlencoded" + * format per Appendix B with a character encoding of UTF-8 in the HTTP + * request entity-body: + * + * grant_type + * REQUIRED. Value MUST be set to "client_credentials". + * + * scope + * OPTIONAL. The scope of the access request as described by + * Section 3.3. + * + * The client MUST authenticate with the authorization server as + * described in Section 3.2.1. + */ + it("authenticates the client with valid credentials", async function () { + const response = new Response(); + const request = createRequest({ + body: { + grant_type: "client_credentials", + scope: enabledScope, + }, + headers: { + authorization: + "Basic " + + Buffer.from(clientDoc.id + ":" + clientDoc.secret).toString( + "base64", + ), + "content-type": "application/x-www-form-urlencoded", + }, + method: "POST", + }); - const token = await oAuth2Server.token(request, response); + const token = await oAuth2Server.token(request, response); - response.status.should.equal(200); - response.headers.should.deep.equal( { 'cache-control': 'no-store', pragma: 'no-cache' }); - response.body.token_type.should.equal('Bearer'); - response.body.access_token.should.equal(token.accessToken); - response.body.expires_in.should.be.a('number'); - response.body.scope.should.eql('read write'); - ('refresh_token' in response.body).should.equal(false); + response.status.should.equal(200); + response.headers.should.deep.equal({ + "cache-control": "no-store", + pragma: "no-cache", + }); + response.body.token_type.should.equal("Bearer"); + response.body.access_token.should.equal(token.accessToken); + response.body.expires_in.should.be.a("number"); + response.body.scope.should.eql("read write"); + ("refresh_token" in response.body).should.equal(false); - token.accessToken.should.be.a('string'); - token.accessTokenExpiresAt.should.be.a('date'); - ('refreshToken' in token).should.equal(false); - ('refreshTokenExpiresAt' in token).should.equal(false); - token.scope.should.eql(['read', 'write']); + token.accessToken.should.be.a("string"); + token.accessTokenExpiresAt.should.be.a("date"); + ("refreshToken" in token).should.equal(false); + ("refreshTokenExpiresAt" in token).should.equal(false); + token.scope.should.eql(["read", "write"]); - db.accessTokens.has(token.accessToken).should.equal(true); - db.refreshTokens.has(token.refreshToken).should.equal(false); - }); + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(false); + }); - /** - * 7. Accessing Protected Resources - * - * The client accesses protected resources by presenting the access - * token to the resource server. The resource server MUST validate the - * access token and ensure that it has not expired and that its scope - * covers the requested resource. The methods used by the resource - * server to validate the access token (as well as any error responses) - * are beyond the scope of this specification but generally involve an - * interaction or coordination between the resource server and the - * authorization server. - */ - it('enables an authenticated request using the access token', async function () { - const [accessToken] = [...db.accessTokens.entries()][0]; - const response = new Response(); - const request = createRequest({ - query: {}, - headers: { - 'authorization': `Bearer ${accessToken}` - }, - method: 'GET', - }); + /** + * 7. Accessing Protected Resources + * + * The client accesses protected resources by presenting the access + * token to the resource server. The resource server MUST validate the + * access token and ensure that it has not expired and that its scope + * covers the requested resource. The methods used by the resource + * server to validate the access token (as well as any error responses) + * are beyond the scope of this specification but generally involve an + * interaction or coordination between the resource server and the + * authorization server. + */ + it("enables an authenticated request using the access token", async function () { + const [accessToken] = [...db.accessTokens.entries()][0]; + const response = new Response(); + const request = createRequest({ + query: {}, + headers: { + authorization: `Bearer ${accessToken}`, + }, + method: "GET", + }); - const token = await oAuth2Server.authenticate(request, response); - token.accessToken.should.equal(accessToken); - token.user.should.deep.equal(userDoc); - token.client.should.deep.equal(clientDoc); - token.scope.should.eql(['read', 'write']); + const token = await oAuth2Server.authenticate(request, response); + token.accessToken.should.equal(accessToken); + token.user.should.deep.equal(userDoc); + token.client.should.deep.equal(clientDoc); + token.scope.should.eql(["read", "write"]); - response.status.should.equal(200); - // there should be no information in the response as it - // should only add information, if permission is denied - response.body.should.deep.equal({}); - response.headers.should.deep.equal({}); - }); - }); + response.status.should.equal(200); + // there should be no information in the response as it + // should only add information, if permission is denied + response.body.should.deep.equal({}); + response.headers.should.deep.equal({}); + }); + }); }); diff --git a/test/compliance/password-grant-type_test.js b/test/compliance/password-grant-type_test.js index d82193af..f8a42207 100644 --- a/test/compliance/password-grant-type_test.js +++ b/test/compliance/password-grant-type_test.js @@ -55,182 +55,183 @@ * developer with additional information about the error. */ -const OAuth2Server = require('../..'); -const DB = require('../helpers/db'); -const createModel = require('../helpers/model'); -const createRequest = require('../helpers/request'); -const Response = require('../../lib/response'); -const crypto = require('crypto'); +const OAuth2Server = require("../.."); +const DB = require("../helpers/db"); +const createModel = require("../helpers/model"); +const createRequest = require("../helpers/request"); +const Response = require("../../lib/response"); +const crypto = require("crypto"); -require('chai').should(); +require("chai").should(); const db = new DB(); const auth = new OAuth2Server({ - model: createModel(db) + model: createModel(db), }); -const user = db.saveUser({ id: 1, username: 'test', password: 'test'}); -const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password'] }); -const scope = 'read write'; - -function createDefaultRequest () { - return createRequest({ - body: { - grant_type: 'password', - username: user.username, - password: user.password, - scope - }, - headers: { - 'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded' - }, - method: 'POST', - }); +const user = db.saveUser({ id: 1, username: "test", password: "test" }); +const client = db.saveClient({ id: "a", secret: "b", grants: ["password"] }); +const scope = "read write"; + +function createDefaultRequest() { + return createRequest({ + body: { + grant_type: "password", + username: user.username, + password: user.password, + scope, + }, + headers: { + authorization: + "Basic " + + Buffer.from(client.id + ":" + client.secret).toString("base64"), + "content-type": "application/x-www-form-urlencoded", + }, + method: "POST", + }); } -describe('PasswordGrantType Compliance', function () { - describe('Authenticate', function () { - it ('Succesfull authorization', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - const token = await auth.token(request, response, {}); - response.body.token_type.should.equal('Bearer'); - response.body.access_token.should.equal(token.accessToken); - response.body.refresh_token.should.equal(token.refreshToken); - response.body.expires_in.should.be.a('number'); - response.body.scope.should.eql('read write'); - - token.accessToken.should.be.a('string'); - token.refreshToken.should.be.a('string'); - token.accessTokenExpiresAt.should.be.a('date'); - token.refreshTokenExpiresAt.should.be.a('date'); - token.scope.should.eql(['read', 'write']); - - db.accessTokens.has(token.accessToken).should.equal(true); - db.refreshTokens.has(token.refreshToken).should.equal(true); - }); - - it ('Succesfull authorization and authentication', async function () { - const tokenRequest = createDefaultRequest(); - const tokenResponse = new Response({}); - - const token = await auth.token(tokenRequest, tokenResponse, {}); - - const authenticationRequest = createRequest({ - body: {}, - headers: { - 'Authorization': `Bearer ${token.accessToken}` - }, - method: 'GET', - query: {} - }); - const authenticationResponse = new Response({}); - - const authenticated = await auth.authenticate( - authenticationRequest, - authenticationResponse, - {}); - - authenticated.scope.should.eql(['read', 'write']); - authenticated.user.should.be.an('object'); - authenticated.client.should.be.an('object'); - }); - - it ('Username missing', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.body.username; - - await auth.token(request, response, {}) - .catch(err => { - err.name.should.equal('invalid_request'); - }); - }); - - it ('Password missing', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.body.password; - - await auth.token(request, response, {}) - .catch(err => { - err.name.should.equal('invalid_request'); - }); - }); - - it ('Wrong username', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - request.body.username = 'wrong'; - - await auth.token(request, response, {}) - .catch(err => { - err.name.should.equal('invalid_grant'); - }); - }); - - it ('Wrong password', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - request.body.password = 'wrong'; - - await auth.token(request, response, {}) - .catch(err => { - err.name.should.equal('invalid_grant'); - }); - }); - - it ('Client not found', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - const clientId = crypto.randomBytes(4).toString('hex'); - const clientSecret = crypto.randomBytes(4).toString('hex'); - - request.headers.authorization = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); - - await auth.token(request, response, {}) - .catch(err => { - err.name.should.equal('invalid_client'); - }); - }); - - it ('Client secret not required', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.body.client_secret; - - const token = await auth.token(request, response, { - requireClientAuthentication: { - password: false - } - }); - - token.accessToken.should.be.a('string'); - }); - - it ('Client secret required', async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.body.client_secret; - - await auth.token(request, response, { - requireClientAuthentication: { - password: false - } - }) - .catch(err => { - err.name.should.equal('invalid_client'); - }); - }); - }); +describe("PasswordGrantType Compliance", function () { + describe("Authenticate", function () { + it("Succesfull authorization", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + const token = await auth.token(request, response, {}); + response.body.token_type.should.equal("Bearer"); + response.body.access_token.should.equal(token.accessToken); + response.body.refresh_token.should.equal(token.refreshToken); + response.body.expires_in.should.be.a("number"); + response.body.scope.should.eql("read write"); + + token.accessToken.should.be.a("string"); + token.refreshToken.should.be.a("string"); + token.accessTokenExpiresAt.should.be.a("date"); + token.refreshTokenExpiresAt.should.be.a("date"); + token.scope.should.eql(["read", "write"]); + + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(true); + }); + + it("Succesfull authorization and authentication", async function () { + const tokenRequest = createDefaultRequest(); + const tokenResponse = new Response({}); + + const token = await auth.token(tokenRequest, tokenResponse, {}); + + const authenticationRequest = createRequest({ + body: {}, + headers: { + Authorization: `Bearer ${token.accessToken}`, + }, + method: "GET", + query: {}, + }); + const authenticationResponse = new Response({}); + + const authenticated = await auth.authenticate( + authenticationRequest, + authenticationResponse, + {}, + ); + + authenticated.scope.should.eql(["read", "write"]); + authenticated.user.should.be.an("object"); + authenticated.client.should.be.an("object"); + }); + + it("Username missing", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.username; + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal("invalid_request"); + }); + }); + + it("Password missing", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.password; + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal("invalid_request"); + }); + }); + + it("Wrong username", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.body.username = "wrong"; + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal("invalid_grant"); + }); + }); + + it("Wrong password", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.body.password = "wrong"; + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal("invalid_grant"); + }); + }); + + it("Client not found", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + const clientId = crypto.randomBytes(4).toString("hex"); + const clientSecret = crypto.randomBytes(4).toString("hex"); + + request.headers.authorization = + "Basic " + + Buffer.from(`${clientId}:${clientSecret}`).toString("base64"); + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal("invalid_client"); + }); + }); + + it("Client secret not required", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.client_secret; + + const token = await auth.token(request, response, { + requireClientAuthentication: { + password: false, + }, + }); + + token.accessToken.should.be.a("string"); + }); + + it("Client secret required", async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.client_secret; + + await auth + .token(request, response, { + requireClientAuthentication: { + password: false, + }, + }) + .catch((err) => { + err.name.should.equal("invalid_client"); + }); + }); + }); }); diff --git a/test/compliance/pkce_test.js b/test/compliance/pkce_test.js index fa46b8fd..b3ea63e2 100644 --- a/test/compliance/pkce_test.js +++ b/test/compliance/pkce_test.js @@ -36,639 +36,661 @@ * violate RFC 7636 itself. */ -const OAuth2Server = require('../..'); -const DB = require('../helpers/db'); -const createModel = require('../helpers/model'); -const createRequest = require('../helpers/request'); -const Response = require('../../lib/response'); -const { base64URLEncode } = require('../../lib/utils/string-util'); -const { createHash } = require('../../lib/utils/crypto-util'); -const { InvalidRequestError } = require('../../index'); -const ServerError = require('../../lib/errors/server-error'); -const InvalidGrantError = require('../../lib/errors/invalid-grant-error'); -require('chai').should(); +const OAuth2Server = require("../.."); +const DB = require("../helpers/db"); +const createModel = require("../helpers/model"); +const createRequest = require("../helpers/request"); +const Response = require("../../lib/response"); +const { base64URLEncode } = require("../../lib/utils/string-util"); +const { createHash } = require("../../lib/utils/crypto-util"); +const { InvalidRequestError } = require("../../index"); +const ServerError = require("../../lib/errors/server-error"); +const InvalidGrantError = require("../../lib/errors/invalid-grant-error"); +require("chai").should(); /** * Compute the S256 code_challenge for a given verifier, * using the same logic the server uses internally. */ -function computeS256Challenge (verifier) { - const hash = createHash({ data: verifier }); - return base64URLEncode(hash); +function computeS256Challenge(verifier) { + const hash = createHash({ data: verifier }); + return base64URLEncode(hash); } -describe('PKCE Compliance (RFC 7636)', function () { - // --------------------------------------------------------------- - // Shared fixtures - // --------------------------------------------------------------- - let db, oAuth2Server; - - const userDoc = { id: 'pkce-user-1', username: 'pkceuser', password: 'pkcepass' }; - const clientDoc = { - id: 'pkce-client', - secret: 'pkce-secret', - grants: ['authorization_code'], - redirectUris: ['https://client.example/callback'] - }; - - /** - * Helper: seed a fresh authorization code into the DB that carries - * a PKCE code challenge (S256). - * @param {string} verifier The code_verifier to use for the code_challenge. Should be a valid string but can be weak (e.g. 1 char) to demonstrate vulnerabilities. - * @param {string} codeValue Optional code value to use (for testing). If not provided, a random one will be generated. - * @param {string} method Optional code_challenge_method to use (default "S256"). For testing the "plain" method, set this to "plain" and ensure the verifier is the same as the challenge. - * @return {object} The authorization code document that was seeded into the DB. - */ - function seedAuthorizationCode (verifier, codeValue, method = 'S256') { - codeValue = codeValue || 'auth-code-' + Math.random().toString(36).slice(2); - const codeChallenge = computeS256Challenge(verifier); - const codeDoc = { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), // 1 min from now - redirectUri: 'https://client.example/callback', - client: clientDoc, - user: userDoc, - scope: ['read'], - codeChallenge, - codeChallengeMethod: method - }; - // store in DB so getAuthorizationCode can find it - db.authorizationCodes.set(codeValue, codeDoc); - return codeDoc; - } - - /** - * Helper: build a token request with the given code and verifier. - * @param {string} code The authorization code to exchange. - * @param {string} codeVerifier The code_verifier to include in the request. - * @returns {Request} The constructed request object. - */ - function tokenRequest (code, codeVerifier) { - return createRequest({ - body: { - grant_type: 'authorization_code', - code, - redirect_uri: 'https://client.example/callback', - code_verifier: codeVerifier - }, - headers: { - 'authorization': 'Basic ' + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded' - }, - method: 'POST' - }); - } - - beforeEach(function () { - db = new DB(); - - // We need authorizationCodes storage on the DB helper - db.authorizationCodes = new Map(); - - db.saveUser(userDoc); - db.saveClient(clientDoc); - - const baseModel = createModel(db); - - oAuth2Server = new OAuth2Server({ - model: { - ...baseModel, - - // --- authorization-code model methods --- - getAuthorizationCode: async function (authorizationCode) { - return db.authorizationCodes.get(authorizationCode) || null; - }, - - saveAuthorizationCode: async function (code, client, user) { - const doc = { ...code, client, user }; - db.authorizationCodes.set(code.authorizationCode, doc); - return doc; - }, - - revokeAuthorizationCode: async function (code) { - return db.authorizationCodes.delete(code.authorizationCode); - }, - - validateScope: async function (user, client, scope) { - return scope; - } - } - }); - }); - - // ================================================================== - // Vulnerability 1 – RFC 7636 §4.1 ABNF not enforced on code_verifier - // - // Note: §4.1 ABNF (`43*128unreserved`) is a client-side requirement. - // §4.6 only mandates hash-and-compare on the server. Enforcing the - // ABNF server-side is defense-in-depth to guarantee the ≥256-bit - // entropy minimum described in Appendix B. - // ================================================================== - describe('attack scenario: server accepts RFC7636-invalid code_verifier values', function () { - /** - * RFC 7636 §4.1 (client requirement): - * code-verifier = 43*128unreserved - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * - * §4.6 (server verification) only says to hash the verifier and - * compare with the stored code_challenge. It does NOT explicitly - * require the server to reject ABNF-invalid verifiers. - * - * However, accepting short/weak verifiers undermines the security - * model: Appendix B depends on ≥256 bits of entropy (which requires - * at least 43 unreserved characters). Server-side ABNF enforcement - * is therefore essential defense-in-depth. - */ - it('should reject a code_verifier shorter than 43 characters', async () => { - const shortVerifier = 'z'; // 1 char – clearly invalid per ABNF - const code = seedAuthorizationCode(shortVerifier); - const request = tokenRequest(code.authorizationCode, shortVerifier); - const response = new Response(); - - // The server should reject this because "z" does not satisfy - // the §4.1 ABNF (43..128 unreserved chars). Although §4.6 does - // not mandate server-side ABNF checks, accepting weak verifiers - // breaks the entropy guarantee of Appendix B. - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `code_verifier`'); - } - - // This assertion documents the a token IS issued - // for an invalid verifier. When the fix is applied this will - // correctly throw, making the test pass again (flip the assertion). - if (tokenIssued) { - throw new Error( - 'Server issued a token for a 1-character code_verifier ("z"). ' + - 'RFC 7636 §4.1 ABNF requires 43..128 unreserved characters; accepting shorter ' + - 'values breaks the entropy guarantee of Appendix B.' - ); - } - }); - - it('should reject a code_verifier of 42 characters (one below minimum)', async () => { - // 42 characters – one below the ABNF minimum of 43 - const shortVerifier = 'a'.repeat(42); - const code = seedAuthorizationCode(shortVerifier); - const request = tokenRequest(code.authorizationCode, shortVerifier); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `code_verifier`'); - } - - if (tokenIssued) { - throw new Error( - 'Server issued a token for a 42-character code_verifier. ' + - 'RFC 7636 §4.1 ABNF minimum is 43 characters; server-side enforcement ' + - 'is needed to preserve the entropy guarantee of Appendix B.' - ); - } - }); - - it('should reject a code_verifier of 129 characters (one above maximum)', async () => { - // 129 characters – one above the ABNF maximum of 128 - const longVerifier = 'b'.repeat(129); - const code = seedAuthorizationCode(longVerifier); - const request = tokenRequest(code.authorizationCode, longVerifier); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `code_verifier`'); - } - - if (tokenIssued) { - throw new Error( - 'Server issued a token for a 129-character code_verifier. ' + - 'RFC 7636 §4.1 ABNF maximum is 128 characters; server-side enforcement ' + - 'is needed to preserve the entropy guarantee of Appendix B.' - ); - } - }); - - it('should reject a code_verifier with forbidden characters', async () => { - // Contains spaces and special chars that are not in the unreserved set - const badVerifier = 'a'.repeat(42) + ' '; // 43 chars but includes a space - const code = seedAuthorizationCode(badVerifier); - const request = tokenRequest(code.authorizationCode, badVerifier); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `code_verifier`'); - } - - if (tokenIssued) { - throw new Error( - 'Server issued a token for a code_verifier containing ' + - 'forbidden characters (space). RFC 7636 §4.1 restricts to unreserved characters.' - ); - } - }); - }); - - // ================================================================= - // Vulnerability 2 – Authorization code not revoked on failed PKCE, - // enabling brute-force guessing - // - // Note: RFC 7636 is silent on code revocation after failed - // verification. RFC 6749 §4.1.2 says a code used "more than once" - // must be denied, but whether a failed PKCE attempt constitutes - // "use" is ambiguous. Revoking on failure is a security best - // practice to prevent online brute-force of the verifier. - // ================================================================= - describe('attack scenario: authorization code survives failed PKCE verification attempts', function () { - /** - * If an attacker intercepts an authorization code, they can - * repeatedly guess code_verifier values. Because the server only - * revokes the code AFTER successful PKCE verification (in - * handle()), every failed attempt leaves the code intact for the - * next guess. - * - * Neither RFC 7636 nor RFC 6749 explicitly mandate revocation on - * failed verification, but without it the authorization code is - * replayable for unlimited brute-force attempts — a clear security - * weakness. - */ - it('should revoke the authorization code on first failed verifier attempt', async () => { - const realVerifier = 'z'; // weak, but accepted by current implementation - const code = seedAuthorizationCode(realVerifier); - - // First attempt with a wrong verifier – should fail - const badRequest = tokenRequest(code.authorizationCode, 'a'); - const badResponse = new Response(); - - // before - const codeExists = db.authorizationCodes.has(code.authorizationCode); - codeExists.should.equal(true, 'Precondition failed: seeded authorization code should exist in DB'); - - try { - await oAuth2Server.token(badRequest, badResponse); - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `code_verifier`'); - } - - // After a failed PKCE attempt the authorization code should have - // been revoked (consumed) to prevent further guessing. - const codeStillExists = db.authorizationCodes.has(code.authorizationCode); - - if (codeStillExists) { - throw new Error( - 'Authorization code was NOT revoked after a failed ' + - 'code_verifier attempt. An attacker can keep guessing.' - ); - } - }); - - it('should not allow brute-forcing a weak code_verifier by retrying with the same authorization code', async () => { - // Use a single-char verifier so the search space is tiny - const realVerifier = 'z'; - const code = seedAuthorizationCode(realVerifier); - - const alphabet = 'abcdefghijklmnopqrstuvwxyz'; - let tokenIssued = false; - let successfulGuess = null; - let tries = 0; - - for (const guess of alphabet) { - tries++; - const request = tokenRequest(code.authorizationCode, guess); - const response = new Response(); - - try { - const token = await oAuth2Server.token(request, response); - if (token && token.accessToken) { - tokenIssued = true; - successfulGuess = guess; - break; - } - } catch (e) { - // wrong guess – continue brute-forcing - } - } - - if (tokenIssued) { - throw new Error( - `Brute-forced code_verifier in ${tries} tries ` + - `(guess="${successfulGuess}"). The authorization code was not ` + - 'consumed after failed attempts, allowing online guessing.' - ); - } - }); - - it('should prevent a legitimate 43-char verifier code to be brute-forceable when code is not revoked on failure', async () => { - // Use a valid-length verifier to show the code-reuse issue - // independently of the ABNF length check - const validVerifier = 'A'.repeat(43); - const code = seedAuthorizationCode(validVerifier); - - // Attempt 1: wrong verifier - const wrongRequest = tokenRequest(code.authorizationCode, 'B'.repeat(43)); - const wrongResponse = new Response(); - - try { - await oAuth2Server.token(wrongRequest, wrongResponse); - } catch (e) { - // Wrong verifier rejected - e.should.be.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: code verifier is invalid'); - } - - // Attempt 2: correct verifier but should fail because code was revoked - const correctRequest = tokenRequest(code.authorizationCode, validVerifier); - const correctResponse = new Response(); - - let tokenIssued = false; - - try { - const token = await oAuth2Server.token(correctRequest, correctResponse); - if (token && token.accessToken) { - tokenIssued = true; - } - } catch (e) { - // This is the correct behaviour after fix: code was revoked and is invalid now - e.should.be.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - } - - if (tokenIssued) { - throw new Error( - 'Authorization code was still valid after a prior ' + - 'failed PKCE attempt. The code should have been revoked on the first ' + - 'failed verification to prevent further guessing.' - ); - } - }); - }); - - // ================================================================= - // Vulnerability 3 - PKCE defaults to "plain" method instead of S256 - // - // Note: RFC 7636 §4.3 specifies that the server assumes "plain" - // when code_challenge_method is absent, and the server MUST support - // "plain". So defaulting to "plain" is technically *RFC-compliant*. - // - // However, "plain" means code_challenge === code_verifier, offering - // zero cryptographic protection. Both the OAuth 2.0 Security BCP - // (draft-ietf-oauth-security-topics §2.1.1) and OAuth 2.1 - // (draft-ietf-oauth-v2-1) deprecate "plain" in favour of S256. - // - // The tests below flag "plain" as a weakness even though it does - // not violate RFC 7636 itself. - // ================================================================= - describe('attack scenario: PKCE defaults to plain method instead of S256 ', function () { - /** - * RFC 7636 §4.2 (client obligation): - * "If the client is capable of using 'S256', it MUST use 'S256'" - * - * RFC 7636 §4.3 (server behaviour): - * The server assumes "plain" when code_challenge_method is absent - * and the server MUST support "plain". This makes defaulting to - * "plain" technically RFC-compliant. - * - * The problem: with "plain", code_challenge === code_verifier. - * Intercepting the authorization request reveals the verifier - * directly — defeating PKCE's purpose for public clients. - * - * Modern guidance (OAuth 2.0 Security BCP §2.1.1, OAuth 2.1) - * deprecates "plain" and recommends servers require S256. - */ - it('should reject or upgrade "plain" PKCE at the token endpoint (beyond-spec hardening)', async () => { - const verifier = 'a'.repeat(43); // valid ABNF-length verifier - - // With "plain" method, the code_challenge IS the code_verifier. - // We seed an authorization code using "plain" (which is what the - // server would store when code_challenge_method is omitted per - // RFC 7636 §4.3). - const codeValue = 'auth-code-plain-default-' + Math.random().toString(36).slice(2); - const codeDoc = { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), - redirectUri: 'https://client.example/callback', - client: clientDoc, - user: userDoc, - scope: ['read'], - codeChallenge: verifier, // plain: challenge === verifier - codeChallengeMethod: 'plain' // RFC 7636 §4.3 default - }; - db.authorizationCodes.set(codeValue, codeDoc); - - // Token exchange: provide the verifier in plain text - const request = tokenRequest(codeValue, verifier); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - // would be expected if plain were rejected - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); - } - - // Note: accepting "plain" is RFC 7636-compliant (§4.3 says the - // server MUST support "plain"). However, "plain" means - // code_challenge === code_verifier — zero cryptographic - // protection. OAuth 2.0 Security BCP §2.1.1 and OAuth 2.1 - // deprecate "plain" in favour of S256. - if (tokenIssued) { - throw new Error( - 'Server issued a token using "plain" PKCE method. ' + - 'While RFC 7636 §4.3 requires server support for "plain", the OAuth 2.0 ' + - 'Security BCP and OAuth 2.1 deprecate it because code_challenge === code_verifier ' + - 'offers zero cryptographic protection.' - ); - } - }); - - it('should reject defaults to plain by default (beyond-spec hardening)', async () => { - // Create a separate OAuth2Server instance with enablePlainPKCE enabled. - // When this option is true, the server rejects any PKCE flow that uses - // the "plain" code_challenge_method — even though RFC 7636 §4.3 says - // the server MUST support "plain". This is a hardening measure - // aligned with OAuth 2.0 Security BCP §2.1.1 and OAuth 2.1. which deprecate - // "plain" due to its lack of cryptographic protection. - const baseModel = createModel(db); - const strictServer = new OAuth2Server({ - enablePlainPKCE: false, - model: { - ...baseModel, - getAuthorizationCode: async function (authorizationCode) { - return db.authorizationCodes.get(authorizationCode) || null; - }, - saveAuthorizationCode: async function (code, client, user) { - const doc = { ...code, client, user }; - db.authorizationCodes.set(code.authorizationCode, doc); - return doc; - }, - revokeAuthorizationCode: async function (code) { - return db.authorizationCodes.delete(code.authorizationCode); - }, - validateScope: async function (user, client, scope) { - return scope; - } - } - }); - - const verifier = 'a'.repeat(43); // valid ABNF-length verifier - const codeValue = 'auth-code-reject-plain-' + Math.random().toString(36).slice(2); - db.authorizationCodes.set(codeValue, { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), - redirectUri: 'https://client.example/callback', - client: clientDoc, - user: userDoc, - scope: ['read'], - codeChallenge: verifier, // plain: challenge === verifier - codeChallengeMethod: 'plain' - }); - - const request = tokenRequest(codeValue, verifier); - const response = new Response(); - - let tokenIssued = false; - let error = null; - - try { - await strictServer.token(request, response); - tokenIssued = true; - } catch (e) { - error = e; - } - - if (tokenIssued) { - throw new Error( - 'Server with enablePlainPKCE=false still issued ' + - 'a token using "plain" PKCE method. The option should cause the server to ' + - 'reject any plain code_challenge_method.' - ); - } - - // When rejected correctly, the server should respond with an error - // indicating that the plain method is not allowed. - (error !== null).should.equal(true); - }); - - it('should not allow an attacker who intercepts the authorize request to use the plain code_challenge as verifier', async () => { - // Scenario: a public client sends an authorize request without - // specifying code_challenge_method. The server defaults to "plain", - // storing code_challenge = code_verifier. - // - // An attacker who intercepts the authorize redirect (which contains - // code_challenge in the query string) now knows the code_verifier. - const verifier = 'x'.repeat(50); - - // Simulate what the server stores when code_challenge_method is - // omitted (defaults to "plain"): code_challenge = verifier - const stolenChallenge = verifier; // attacker reads this from the authorize request - - const codeValue = 'auth-code-stolen-' + Math.random().toString(36).slice(2); - db.authorizationCodes.set(codeValue, { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), - redirectUri: 'https://client.example/callback', - client: clientDoc, - user: userDoc, - scope: ['read'], - codeChallenge: verifier, - codeChallengeMethod: 'plain' - }); - - // The attacker uses the stolen code_challenge directly as code_verifier - const request = tokenRequest(codeValue, stolenChallenge); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); - } - - if (tokenIssued) { - throw new Error( - 'Attacker redeemed an authorization code by using ' + - 'the intercepted code_challenge as code_verifier (plain method). ' + - 'This defeats PKCE entirely for public clients.' - ); - } - }); - - it('should not allow an attacker who intercepts the authorize request to use an invalid code_challenge as verifier', async () => { - // Scenario: a public client sends an authorize request without - // specifying code_challenge_method. The server defaults to "plain", - // storing code_challenge = code_verifier. - // - // An attacker who intercepts the authorize redirect (which contains - // code_challenge in the query string) now knows the code_verifier. - const verifier = 'x'.repeat(50); - - // Simulate what the server stores when code_challenge_method is - // omitted (defaults to "plain"): code_challenge = verifier - const stolenChallenge = verifier; // attacker reads this from the authorize request - - const codeValue = 'auth-code-stolen-' + Math.random().toString(36).slice(2); - db.authorizationCodes.set(codeValue, { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), - redirectUri: 'https://client.example/callback', - client: clientDoc, - user: userDoc, - scope: ['read'], - codeChallenge: verifier, - codeChallengeMethod: 'forged-xyz' // invalid method stored in DB that could cause a "plain" fallback if not handled properly - }); - - // The attacker uses the stolen code_challenge directly as code_verifier - const request = tokenRequest(codeValue, stolenChallenge); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - // this is not part of the standard which is why we throw a generic ServerError - e.should.be.instanceOf(ServerError); - e.message.should.equal('Server error: no valid hash algorithm available to verify `code_verifier`'); - } - - if (tokenIssued) { - throw new Error( - 'Attacker redeemed an authorization code by using ' + - 'the intercepted code_challenge as code_verifier (custom method). ' + - 'This defeats PKCE entirely for public clients.' - ); - } - }); - }); +describe("PKCE Compliance (RFC 7636)", function () { + // --------------------------------------------------------------- + // Shared fixtures + // --------------------------------------------------------------- + let db, oAuth2Server; + + const userDoc = { + id: "pkce-user-1", + username: "pkceuser", + password: "pkcepass", + }; + const clientDoc = { + id: "pkce-client", + secret: "pkce-secret", + grants: ["authorization_code"], + redirectUris: ["https://client.example/callback"], + }; + + /** + * Helper: seed a fresh authorization code into the DB that carries + * a PKCE code challenge (S256). + * @param {string} verifier The code_verifier to use for the code_challenge. Should be a valid string but can be weak (e.g. 1 char) to demonstrate vulnerabilities. + * @param {string} codeValue Optional code value to use (for testing). If not provided, a random one will be generated. + * @param {string} method Optional code_challenge_method to use (default "S256"). For testing the "plain" method, set this to "plain" and ensure the verifier is the same as the challenge. + * @return {object} The authorization code document that was seeded into the DB. + */ + function seedAuthorizationCode(verifier, codeValue, method = "S256") { + codeValue = codeValue || "auth-code-" + Math.random().toString(36).slice(2); + const codeChallenge = computeS256Challenge(verifier); + const codeDoc = { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), // 1 min from now + redirectUri: "https://client.example/callback", + client: clientDoc, + user: userDoc, + scope: ["read"], + codeChallenge, + codeChallengeMethod: method, + }; + // store in DB so getAuthorizationCode can find it + db.authorizationCodes.set(codeValue, codeDoc); + return codeDoc; + } + + /** + * Helper: build a token request with the given code and verifier. + * @param {string} code The authorization code to exchange. + * @param {string} codeVerifier The code_verifier to include in the request. + * @returns {Request} The constructed request object. + */ + function tokenRequest(code, codeVerifier) { + return createRequest({ + body: { + grant_type: "authorization_code", + code, + redirect_uri: "https://client.example/callback", + code_verifier: codeVerifier, + }, + headers: { + authorization: + "Basic " + + Buffer.from(clientDoc.id + ":" + clientDoc.secret).toString("base64"), + "content-type": "application/x-www-form-urlencoded", + }, + method: "POST", + }); + } + + beforeEach(function () { + db = new DB(); + + // We need authorizationCodes storage on the DB helper + db.authorizationCodes = new Map(); + + db.saveUser(userDoc); + db.saveClient(clientDoc); + + const baseModel = createModel(db); + + oAuth2Server = new OAuth2Server({ + model: { + ...baseModel, + + // --- authorization-code model methods --- + getAuthorizationCode: async function (authorizationCode) { + return db.authorizationCodes.get(authorizationCode) || null; + }, + + saveAuthorizationCode: async function (code, client, user) { + const doc = { ...code, client, user }; + db.authorizationCodes.set(code.authorizationCode, doc); + return doc; + }, + + revokeAuthorizationCode: async function (code) { + return db.authorizationCodes.delete(code.authorizationCode); + }, + + validateScope: async function (user, client, scope) { + return scope; + }, + }, + }); + }); + + // ================================================================== + // Vulnerability 1 – RFC 7636 §4.1 ABNF not enforced on code_verifier + // + // Note: §4.1 ABNF (`43*128unreserved`) is a client-side requirement. + // §4.6 only mandates hash-and-compare on the server. Enforcing the + // ABNF server-side is defense-in-depth to guarantee the ≥256-bit + // entropy minimum described in Appendix B. + // ================================================================== + describe("attack scenario: server accepts RFC7636-invalid code_verifier values", function () { + /** + * RFC 7636 §4.1 (client requirement): + * code-verifier = 43*128unreserved + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * + * §4.6 (server verification) only says to hash the verifier and + * compare with the stored code_challenge. It does NOT explicitly + * require the server to reject ABNF-invalid verifiers. + * + * However, accepting short/weak verifiers undermines the security + * model: Appendix B depends on ≥256 bits of entropy (which requires + * at least 43 unreserved characters). Server-side ABNF enforcement + * is therefore essential defense-in-depth. + */ + it("should reject a code_verifier shorter than 43 characters", async () => { + const shortVerifier = "z"; // 1 char – clearly invalid per ABNF + const code = seedAuthorizationCode(shortVerifier); + const request = tokenRequest(code.authorizationCode, shortVerifier); + const response = new Response(); + + // The server should reject this because "z" does not satisfy + // the §4.1 ABNF (43..128 unreserved chars). Although §4.6 does + // not mandate server-side ABNF checks, accepting weak verifiers + // breaks the entropy guarantee of Appendix B. + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `code_verifier`"); + } + + // This assertion documents the a token IS issued + // for an invalid verifier. When the fix is applied this will + // correctly throw, making the test pass again (flip the assertion). + if (tokenIssued) { + throw new Error( + 'Server issued a token for a 1-character code_verifier ("z"). ' + + "RFC 7636 §4.1 ABNF requires 43..128 unreserved characters; accepting shorter " + + "values breaks the entropy guarantee of Appendix B.", + ); + } + }); + + it("should reject a code_verifier of 42 characters (one below minimum)", async () => { + // 42 characters – one below the ABNF minimum of 43 + const shortVerifier = "a".repeat(42); + const code = seedAuthorizationCode(shortVerifier); + const request = tokenRequest(code.authorizationCode, shortVerifier); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `code_verifier`"); + } + + if (tokenIssued) { + throw new Error( + "Server issued a token for a 42-character code_verifier. " + + "RFC 7636 §4.1 ABNF minimum is 43 characters; server-side enforcement " + + "is needed to preserve the entropy guarantee of Appendix B.", + ); + } + }); + + it("should reject a code_verifier of 129 characters (one above maximum)", async () => { + // 129 characters – one above the ABNF maximum of 128 + const longVerifier = "b".repeat(129); + const code = seedAuthorizationCode(longVerifier); + const request = tokenRequest(code.authorizationCode, longVerifier); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `code_verifier`"); + } + + if (tokenIssued) { + throw new Error( + "Server issued a token for a 129-character code_verifier. " + + "RFC 7636 §4.1 ABNF maximum is 128 characters; server-side enforcement " + + "is needed to preserve the entropy guarantee of Appendix B.", + ); + } + }); + + it("should reject a code_verifier with forbidden characters", async () => { + // Contains spaces and special chars that are not in the unreserved set + const badVerifier = "a".repeat(42) + " "; // 43 chars but includes a space + const code = seedAuthorizationCode(badVerifier); + const request = tokenRequest(code.authorizationCode, badVerifier); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `code_verifier`"); + } + + if (tokenIssued) { + throw new Error( + "Server issued a token for a code_verifier containing " + + "forbidden characters (space). RFC 7636 §4.1 restricts to unreserved characters.", + ); + } + }); + }); + + // ================================================================= + // Vulnerability 2 – Authorization code not revoked on failed PKCE, + // enabling brute-force guessing + // + // Note: RFC 7636 is silent on code revocation after failed + // verification. RFC 6749 §4.1.2 says a code used "more than once" + // must be denied, but whether a failed PKCE attempt constitutes + // "use" is ambiguous. Revoking on failure is a security best + // practice to prevent online brute-force of the verifier. + // ================================================================= + describe("attack scenario: authorization code survives failed PKCE verification attempts", function () { + /** + * If an attacker intercepts an authorization code, they can + * repeatedly guess code_verifier values. Because the server only + * revokes the code AFTER successful PKCE verification (in + * handle()), every failed attempt leaves the code intact for the + * next guess. + * + * Neither RFC 7636 nor RFC 6749 explicitly mandate revocation on + * failed verification, but without it the authorization code is + * replayable for unlimited brute-force attempts — a clear security + * weakness. + */ + it("should revoke the authorization code on first failed verifier attempt", async () => { + const realVerifier = "z"; // weak, but accepted by current implementation + const code = seedAuthorizationCode(realVerifier); + + // First attempt with a wrong verifier – should fail + const badRequest = tokenRequest(code.authorizationCode, "a"); + const badResponse = new Response(); + + // before + const codeExists = db.authorizationCodes.has(code.authorizationCode); + codeExists.should.equal( + true, + "Precondition failed: seeded authorization code should exist in DB", + ); + + try { + await oAuth2Server.token(badRequest, badResponse); + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `code_verifier`"); + } + + // After a failed PKCE attempt the authorization code should have + // been revoked (consumed) to prevent further guessing. + const codeStillExists = db.authorizationCodes.has(code.authorizationCode); + + if (codeStillExists) { + throw new Error( + "Authorization code was NOT revoked after a failed " + + "code_verifier attempt. An attacker can keep guessing.", + ); + } + }); + + it("should not allow brute-forcing a weak code_verifier by retrying with the same authorization code", async () => { + // Use a single-char verifier so the search space is tiny + const realVerifier = "z"; + const code = seedAuthorizationCode(realVerifier); + + const alphabet = "abcdefghijklmnopqrstuvwxyz"; + let tokenIssued = false; + let successfulGuess = null; + let tries = 0; + + for (const guess of alphabet) { + tries++; + const request = tokenRequest(code.authorizationCode, guess); + const response = new Response(); + + try { + const token = await oAuth2Server.token(request, response); + if (token && token.accessToken) { + tokenIssued = true; + successfulGuess = guess; + break; + } + } catch (e) { + // wrong guess – continue brute-forcing + } + } + + if (tokenIssued) { + throw new Error( + `Brute-forced code_verifier in ${tries} tries ` + + `(guess="${successfulGuess}"). The authorization code was not ` + + "consumed after failed attempts, allowing online guessing.", + ); + } + }); + + it("should prevent a legitimate 43-char verifier code to be brute-forceable when code is not revoked on failure", async () => { + // Use a valid-length verifier to show the code-reuse issue + // independently of the ABNF length check + const validVerifier = "A".repeat(43); + const code = seedAuthorizationCode(validVerifier); + + // Attempt 1: wrong verifier + const wrongRequest = tokenRequest(code.authorizationCode, "B".repeat(43)); + const wrongResponse = new Response(); + + try { + await oAuth2Server.token(wrongRequest, wrongResponse); + } catch (e) { + // Wrong verifier rejected + e.should.be.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: code verifier is invalid"); + } + + // Attempt 2: correct verifier but should fail because code was revoked + const correctRequest = tokenRequest( + code.authorizationCode, + validVerifier, + ); + const correctResponse = new Response(); + + let tokenIssued = false; + + try { + const token = await oAuth2Server.token(correctRequest, correctResponse); + if (token && token.accessToken) { + tokenIssued = true; + } + } catch (e) { + // This is the correct behaviour after fix: code was revoked and is invalid now + e.should.be.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: authorization code is invalid"); + } + + if (tokenIssued) { + throw new Error( + "Authorization code was still valid after a prior " + + "failed PKCE attempt. The code should have been revoked on the first " + + "failed verification to prevent further guessing.", + ); + } + }); + }); + + // ================================================================= + // Vulnerability 3 - PKCE defaults to "plain" method instead of S256 + // + // Note: RFC 7636 §4.3 specifies that the server assumes "plain" + // when code_challenge_method is absent, and the server MUST support + // "plain". So defaulting to "plain" is technically *RFC-compliant*. + // + // However, "plain" means code_challenge === code_verifier, offering + // zero cryptographic protection. Both the OAuth 2.0 Security BCP + // (draft-ietf-oauth-security-topics §2.1.1) and OAuth 2.1 + // (draft-ietf-oauth-v2-1) deprecate "plain" in favour of S256. + // + // The tests below flag "plain" as a weakness even though it does + // not violate RFC 7636 itself. + // ================================================================= + describe("attack scenario: PKCE defaults to plain method instead of S256 ", function () { + /** + * RFC 7636 §4.2 (client obligation): + * "If the client is capable of using 'S256', it MUST use 'S256'" + * + * RFC 7636 §4.3 (server behaviour): + * The server assumes "plain" when code_challenge_method is absent + * and the server MUST support "plain". This makes defaulting to + * "plain" technically RFC-compliant. + * + * The problem: with "plain", code_challenge === code_verifier. + * Intercepting the authorization request reveals the verifier + * directly — defeating PKCE's purpose for public clients. + * + * Modern guidance (OAuth 2.0 Security BCP §2.1.1, OAuth 2.1) + * deprecates "plain" and recommends servers require S256. + */ + it('should reject or upgrade "plain" PKCE at the token endpoint (beyond-spec hardening)', async () => { + const verifier = "a".repeat(43); // valid ABNF-length verifier + + // With "plain" method, the code_challenge IS the code_verifier. + // We seed an authorization code using "plain" (which is what the + // server would store when code_challenge_method is omitted per + // RFC 7636 §4.3). + const codeValue = + "auth-code-plain-default-" + Math.random().toString(36).slice(2); + const codeDoc = { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), + redirectUri: "https://client.example/callback", + client: clientDoc, + user: userDoc, + scope: ["read"], + codeChallenge: verifier, // plain: challenge === verifier + codeChallengeMethod: "plain", // RFC 7636 §4.3 default + }; + db.authorizationCodes.set(codeValue, codeDoc); + + // Token exchange: provide the verifier in plain text + const request = tokenRequest(codeValue, verifier); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + // would be expected if plain were rejected + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } + + // Note: accepting "plain" is RFC 7636-compliant (§4.3 says the + // server MUST support "plain"). However, "plain" means + // code_challenge === code_verifier — zero cryptographic + // protection. OAuth 2.0 Security BCP §2.1.1 and OAuth 2.1 + // deprecate "plain" in favour of S256. + if (tokenIssued) { + throw new Error( + 'Server issued a token using "plain" PKCE method. ' + + 'While RFC 7636 §4.3 requires server support for "plain", the OAuth 2.0 ' + + "Security BCP and OAuth 2.1 deprecate it because code_challenge === code_verifier " + + "offers zero cryptographic protection.", + ); + } + }); + + it("should reject defaults to plain by default (beyond-spec hardening)", async () => { + // Create a separate OAuth2Server instance with enablePlainPKCE enabled. + // When this option is true, the server rejects any PKCE flow that uses + // the "plain" code_challenge_method — even though RFC 7636 §4.3 says + // the server MUST support "plain". This is a hardening measure + // aligned with OAuth 2.0 Security BCP §2.1.1 and OAuth 2.1. which deprecate + // "plain" due to its lack of cryptographic protection. + const baseModel = createModel(db); + const strictServer = new OAuth2Server({ + enablePlainPKCE: false, + model: { + ...baseModel, + getAuthorizationCode: async function (authorizationCode) { + return db.authorizationCodes.get(authorizationCode) || null; + }, + saveAuthorizationCode: async function (code, client, user) { + const doc = { ...code, client, user }; + db.authorizationCodes.set(code.authorizationCode, doc); + return doc; + }, + revokeAuthorizationCode: async function (code) { + return db.authorizationCodes.delete(code.authorizationCode); + }, + validateScope: async function (user, client, scope) { + return scope; + }, + }, + }); + + const verifier = "a".repeat(43); // valid ABNF-length verifier + const codeValue = + "auth-code-reject-plain-" + Math.random().toString(36).slice(2); + db.authorizationCodes.set(codeValue, { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), + redirectUri: "https://client.example/callback", + client: clientDoc, + user: userDoc, + scope: ["read"], + codeChallenge: verifier, // plain: challenge === verifier + codeChallengeMethod: "plain", + }); + + const request = tokenRequest(codeValue, verifier); + const response = new Response(); + + let tokenIssued = false; + let error = null; + + try { + await strictServer.token(request, response); + tokenIssued = true; + } catch (e) { + error = e; + } + + if (tokenIssued) { + throw new Error( + "Server with enablePlainPKCE=false still issued " + + 'a token using "plain" PKCE method. The option should cause the server to ' + + "reject any plain code_challenge_method.", + ); + } + + // When rejected correctly, the server should respond with an error + // indicating that the plain method is not allowed. + (error !== null).should.equal(true); + }); + + it("should not allow an attacker who intercepts the authorize request to use the plain code_challenge as verifier", async () => { + // Scenario: a public client sends an authorize request without + // specifying code_challenge_method. The server defaults to "plain", + // storing code_challenge = code_verifier. + // + // An attacker who intercepts the authorize redirect (which contains + // code_challenge in the query string) now knows the code_verifier. + const verifier = "x".repeat(50); + + // Simulate what the server stores when code_challenge_method is + // omitted (defaults to "plain"): code_challenge = verifier + const stolenChallenge = verifier; // attacker reads this from the authorize request + + const codeValue = + "auth-code-stolen-" + Math.random().toString(36).slice(2); + db.authorizationCodes.set(codeValue, { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), + redirectUri: "https://client.example/callback", + client: clientDoc, + user: userDoc, + scope: ["read"], + codeChallenge: verifier, + codeChallengeMethod: "plain", + }); + + // The attacker uses the stolen code_challenge directly as code_verifier + const request = tokenRequest(codeValue, stolenChallenge); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } + + if (tokenIssued) { + throw new Error( + "Attacker redeemed an authorization code by using " + + "the intercepted code_challenge as code_verifier (plain method). " + + "This defeats PKCE entirely for public clients.", + ); + } + }); + + it("should not allow an attacker who intercepts the authorize request to use an invalid code_challenge as verifier", async () => { + // Scenario: a public client sends an authorize request without + // specifying code_challenge_method. The server defaults to "plain", + // storing code_challenge = code_verifier. + // + // An attacker who intercepts the authorize redirect (which contains + // code_challenge in the query string) now knows the code_verifier. + const verifier = "x".repeat(50); + + // Simulate what the server stores when code_challenge_method is + // omitted (defaults to "plain"): code_challenge = verifier + const stolenChallenge = verifier; // attacker reads this from the authorize request + + const codeValue = + "auth-code-stolen-" + Math.random().toString(36).slice(2); + db.authorizationCodes.set(codeValue, { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), + redirectUri: "https://client.example/callback", + client: clientDoc, + user: userDoc, + scope: ["read"], + codeChallenge: verifier, + codeChallengeMethod: "forged-xyz", // invalid method stored in DB that could cause a "plain" fallback if not handled properly + }); + + // The attacker uses the stolen code_challenge directly as code_verifier + const request = tokenRequest(codeValue, stolenChallenge); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + // this is not part of the standard which is why we throw a generic ServerError + e.should.be.instanceOf(ServerError); + e.message.should.equal( + "Server error: no valid hash algorithm available to verify `code_verifier`", + ); + } + + if (tokenIssued) { + throw new Error( + "Attacker redeemed an authorization code by using " + + "the intercepted code_challenge as code_verifier (custom method). " + + "This defeats PKCE entirely for public clients.", + ); + } + }); + }); }); diff --git a/test/compliance/refresh-token-grant-type_test.js b/test/compliance/refresh-token-grant-type_test.js index fd3d40da..db72f880 100644 --- a/test/compliance/refresh-token-grant-type_test.js +++ b/test/compliance/refresh-token-grant-type_test.js @@ -14,7 +14,6 @@ * resource owner. */ - /** * Response * @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1 @@ -57,173 +56,186 @@ * information about the error, used to provide the client * developer with additional information about the error. */ -const OAuth2Server = require('../..'); -const DB = require('../helpers/db'); -const createModel = require('../helpers/model'); -const createRequest = require('../helpers/request'); -const Response = require('../../lib/response'); -const should = require('chai').should(); +const OAuth2Server = require("../.."); +const DB = require("../helpers/db"); +const createModel = require("../helpers/model"); +const createRequest = require("../helpers/request"); +const Response = require("../../lib/response"); +const should = require("chai").should(); -require('chai').should(); +require("chai").should(); const db = new DB(); const auth = new OAuth2Server({ - model: createModel(db) + model: createModel(db), }); -const user = db.saveUser({ id: 1, username: 'test', password: 'test'}); -const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password', 'refresh_token'] }); -const scope = 'read write'; - -function createLoginRequest () { - return createRequest({ - body: { - grant_type: 'password', - username: user.username, - password: user.password, - scope - }, - headers: { - 'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded' - }, - method: 'POST', - }); +const user = db.saveUser({ id: 1, username: "test", password: "test" }); +const client = db.saveClient({ + id: "a", + secret: "b", + grants: ["password", "refresh_token"], +}); +const scope = "read write"; + +function createLoginRequest() { + return createRequest({ + body: { + grant_type: "password", + username: user.username, + password: user.password, + scope, + }, + headers: { + authorization: + "Basic " + + Buffer.from(client.id + ":" + client.secret).toString("base64"), + "content-type": "application/x-www-form-urlencoded", + }, + method: "POST", + }); } -function createRefreshRequest (refresh_token) { - return createRequest({ - method: 'POST', - body: { - grant_type: 'refresh_token', - refresh_token, - scope - }, - headers: { - 'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded' - } - }); +function createRefreshRequest(refresh_token) { + return createRequest({ + method: "POST", + body: { + grant_type: "refresh_token", + refresh_token, + scope, + }, + headers: { + authorization: + "Basic " + + Buffer.from(client.id + ":" + client.secret).toString("base64"), + "content-type": "application/x-www-form-urlencoded", + }, + }); } -describe('RefreshTokenGrantType Compliance', function () { - describe('With scope', function () { - it('Should generate token response', async function () { - const request = createLoginRequest(); - const response = new Response({}); +describe("RefreshTokenGrantType Compliance", function () { + describe("With scope", function () { + it("Should generate token response", async function () { + const request = createLoginRequest(); + const response = new Response({}); - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - const token = await auth.token(refreshRequest, refreshResponse, {}); + const token = await auth.token(refreshRequest, refreshResponse, {}); - refreshResponse.body.token_type.should.equal('Bearer'); - refreshResponse.body.access_token.should.equal(token.accessToken); - refreshResponse.body.refresh_token.should.equal(token.refreshToken); - refreshResponse.body.expires_in.should.be.a('number'); - refreshResponse.body.scope.should.eql('read write'); + refreshResponse.body.token_type.should.equal("Bearer"); + refreshResponse.body.access_token.should.equal(token.accessToken); + refreshResponse.body.refresh_token.should.equal(token.refreshToken); + refreshResponse.body.expires_in.should.be.a("number"); + refreshResponse.body.scope.should.eql("read write"); - token.accessToken.should.be.a('string'); - token.refreshToken.should.be.a('string'); - token.accessTokenExpiresAt.should.be.a('date'); - token.refreshTokenExpiresAt.should.be.a('date'); - token.scope.should.eql(['read', 'write']); + token.accessToken.should.be.a("string"); + token.refreshToken.should.be.a("string"); + token.accessTokenExpiresAt.should.be.a("date"); + token.refreshTokenExpiresAt.should.be.a("date"); + token.scope.should.eql(["read", "write"]); - db.accessTokens.has(token.accessToken).should.equal(true); - db.refreshTokens.has(token.refreshToken).should.equal(true); - }); + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(true); + }); - it('Should throw invalid_grant error', async function () { - const request = createRefreshRequest('invalid'); - const response = new Response({}); + it("Should throw invalid_grant error", async function () { + const request = createRefreshRequest("invalid"); + const response = new Response({}); - await auth.token(request, response, {}) - .then(() => { - throw Error('Should not reach this'); - }).catch(err => { - err.name.should.equal('invalid_grant'); - }); - }); + await auth + .token(request, response, {}) + .then(() => { + throw Error("Should not reach this"); + }) + .catch((err) => { + err.name.should.equal("invalid_grant"); + }); + }); - it('Should throw invalid_scope error', async function () { - const request = createLoginRequest(); - const response = new Response({}); + it("Should throw invalid_scope error", async function () { + const request = createLoginRequest(); + const response = new Response({}); - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - refreshRequest.body.scope = 'invalid'; + refreshRequest.body.scope = "invalid"; - await auth.token(refreshRequest, refreshResponse, {}) - .then(should.fail) - .catch(err => { - err.name.should.equal('invalid_scope'); - }); - }); + await auth + .token(refreshRequest, refreshResponse, {}) + .then(should.fail) + .catch((err) => { + err.name.should.equal("invalid_scope"); + }); + }); - it('Should throw error if requested scope is greater than original scope', async function () { - const request = createLoginRequest(); - const response = new Response({}); + it("Should throw error if requested scope is greater than original scope", async function () { + const request = createLoginRequest(); + const response = new Response({}); - request.body.scope = 'read'; + request.body.scope = "read"; - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - refreshRequest.scope = 'read write'; + refreshRequest.scope = "read write"; - await auth.token(refreshRequest, refreshResponse, {}) - .then(should.fail) - .catch(err => { - err.name.should.equal('invalid_scope'); - }); - }); + await auth + .token(refreshRequest, refreshResponse, {}) + .then(should.fail) + .catch((err) => { + err.name.should.equal("invalid_scope"); + }); + }); - it('Should throw error if a scope is requested without a previous scope', async function () { - const request = createLoginRequest(); - const response = new Response({}); + it("Should throw error if a scope is requested without a previous scope", async function () { + const request = createLoginRequest(); + const response = new Response({}); - delete request.body.scope; + delete request.body.scope; - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - refreshRequest.scope = 'read write'; + refreshRequest.scope = "read write"; - await auth.token(refreshRequest, refreshResponse, {}) - .then(should.fail) - .catch(err => { - err.name.should.equal('invalid_scope'); - }); - }); + await auth + .token(refreshRequest, refreshResponse, {}) + .then(should.fail) + .catch((err) => { + err.name.should.equal("invalid_scope"); + }); + }); - it('Should create refresh token with smaller scope', async function () { - const request = createLoginRequest(); - const response = new Response({}); + it("Should create refresh token with smaller scope", async function () { + const request = createLoginRequest(); + const response = new Response({}); - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - refreshRequest.body.scope = 'read'; + refreshRequest.body.scope = "read"; - const token = await auth.token(refreshRequest, refreshResponse, {}); + const token = await auth.token(refreshRequest, refreshResponse, {}); - refreshResponse.body.token_type.should.equal('Bearer'); - refreshResponse.body.access_token.should.equal(token.accessToken); - refreshResponse.body.refresh_token.should.equal(token.refreshToken); - refreshResponse.body.expires_in.should.be.a('number'); - refreshResponse.body.scope.should.eql('read'); - }); - }); + refreshResponse.body.token_type.should.equal("Bearer"); + refreshResponse.body.access_token.should.equal(token.accessToken); + refreshResponse.body.refresh_token.should.equal(token.refreshToken); + refreshResponse.body.expires_in.should.be.a("number"); + refreshResponse.body.scope.should.eql("read"); + }); + }); }); diff --git a/test/helpers/db.js b/test/helpers/db.js index 147d1741..7d5444fb 100644 --- a/test/helpers/db.js +++ b/test/helpers/db.js @@ -1,70 +1,70 @@ class DB { - constructor () { - this.users = new Map(); - this.clients = []; - this.accessTokens = new Map(); - this.refreshTokens= new Map(); - } + constructor() { + this.users = new Map(); + this.clients = []; + this.accessTokens = new Map(); + this.refreshTokens = new Map(); + } - saveUser (user) { - this.users.set(user.id, user); + saveUser(user) { + this.users.set(user.id, user); - return user; - } + return user; + } - findUser (username, password) { - return Array.from(this.users.values()).find(user => { - return user.username === username && user.password === password; - }); - } + findUser(username, password) { + return Array.from(this.users.values()).find((user) => { + return user.username === username && user.password === password; + }); + } - findUserById (id) { - return this.users.get(id); - } + findUserById(id) { + return this.users.get(id); + } - saveClient (client) { - this.clients.push(client); + saveClient(client) { + this.clients.push(client); - return client; - } + return client; + } - findClient (clientId, clientSecret) { - return this.clients.find(client => { - if (clientSecret) { - return client.id === clientId && client.secret === clientSecret; - } else { - return client.id === clientId; - } - }); - } + findClient(clientId, clientSecret) { + return this.clients.find((client) => { + if (clientSecret) { + return client.id === clientId && client.secret === clientSecret; + } else { + return client.id === clientId; + } + }); + } - findClientById (id) { - return this.clients.find(client => client.id === id); - } + findClientById(id) { + return this.clients.find((client) => client.id === id); + } - saveAccessToken (accessToken, meta) { - this.accessTokens.set(accessToken, meta); - } + saveAccessToken(accessToken, meta) { + this.accessTokens.set(accessToken, meta); + } - findAccessToken (accessToken) { - return this.accessTokens.get(accessToken); - } + findAccessToken(accessToken) { + return this.accessTokens.get(accessToken); + } - deleteAccessToken (accessToken) { - this.accessTokens.delete(accessToken); - } + deleteAccessToken(accessToken) { + this.accessTokens.delete(accessToken); + } - saveRefreshToken (refreshToken, meta) { - this.refreshTokens.set(refreshToken, meta); - } + saveRefreshToken(refreshToken, meta) { + this.refreshTokens.set(refreshToken, meta); + } - findRefreshToken (refreshToken) { - return this.refreshTokens.get(refreshToken); - } + findRefreshToken(refreshToken) { + return this.refreshTokens.get(refreshToken); + } - deleteRefreshToken (refreshToken) { - this.refreshTokens.delete(refreshToken); - } + deleteRefreshToken(refreshToken) { + this.refreshTokens.delete(refreshToken); + } } module.exports = DB; diff --git a/test/helpers/model.js b/test/helpers/model.js index 12507bbf..7faccdc7 100644 --- a/test/helpers/model.js +++ b/test/helpers/model.js @@ -1,99 +1,99 @@ -const Model = require('../../lib/model'); -const scopes = ['read', 'write']; - -function createModel (db) { - async function getUser (username, password) { - return db.findUser(username, password); - } - - async function getClient (clientId, clientSecret) { - return db.findClient(clientId, clientSecret); - } - - async function saveToken (token, client, user) { - if (token.scope && !Array.isArray(token.scope)) { - throw new Error('Scope should internally be an array'); - } - const meta = { - clientId: client.id, - userId: user.id, - scope: token.scope, - accessTokenExpiresAt: token.accessTokenExpiresAt, - refreshTokenExpiresAt: token.refreshTokenExpiresAt - }; - - token.client = client; - token.user = user; - - if (token.accessToken) { - db.saveAccessToken(token.accessToken, meta); - } - - if (token.refreshToken) { - db.saveRefreshToken(token.refreshToken, meta); - } - - return token; - } - - async function getAccessToken (accessToken) { - const meta = db.findAccessToken(accessToken); - - if (!meta) { - return false; - } - if (meta.scope && !Array.isArray(meta.scope)) { - throw new Error('Scope should internally be an array'); - } - return { - accessToken, - accessTokenExpiresAt: meta.accessTokenExpiresAt, - user: db.findUserById(meta.userId), - client: db.findClientById(meta.clientId), - scope: meta.scope - }; - } - - async function getRefreshToken (refreshToken) { - const meta = db.findRefreshToken(refreshToken); - - if (!meta) { - return false; - } - if (meta.scope && !Array.isArray(meta.scope)) { - throw new Error('Scope should internally be an array'); - } - return { - refreshToken, - refreshTokenExpiresAt: meta.refreshTokenExpiresAt, - user: db.findUserById(meta.userId), - client: db.findClientById(meta.clientId), - scope: meta.scope - }; - } - - async function revokeToken (token) { - db.deleteRefreshToken(token.refreshToken); - - return true; - } - - async function verifyScope (token, scope) { - if (!Array.isArray(scope)) { - throw new Error('Scope should internally be an array'); - } - return scope.every(s => scopes.includes(s)); - } - - return Model.from({ - getUser, - getClient, - saveToken, - getAccessToken, - getRefreshToken, - revokeToken, - verifyScope - }); +const Model = require("../../lib/model"); +const scopes = ["read", "write"]; + +function createModel(db) { + async function getUser(username, password) { + return db.findUser(username, password); + } + + async function getClient(clientId, clientSecret) { + return db.findClient(clientId, clientSecret); + } + + async function saveToken(token, client, user) { + if (token.scope && !Array.isArray(token.scope)) { + throw new Error("Scope should internally be an array"); + } + const meta = { + clientId: client.id, + userId: user.id, + scope: token.scope, + accessTokenExpiresAt: token.accessTokenExpiresAt, + refreshTokenExpiresAt: token.refreshTokenExpiresAt, + }; + + token.client = client; + token.user = user; + + if (token.accessToken) { + db.saveAccessToken(token.accessToken, meta); + } + + if (token.refreshToken) { + db.saveRefreshToken(token.refreshToken, meta); + } + + return token; + } + + async function getAccessToken(accessToken) { + const meta = db.findAccessToken(accessToken); + + if (!meta) { + return false; + } + if (meta.scope && !Array.isArray(meta.scope)) { + throw new Error("Scope should internally be an array"); + } + return { + accessToken, + accessTokenExpiresAt: meta.accessTokenExpiresAt, + user: db.findUserById(meta.userId), + client: db.findClientById(meta.clientId), + scope: meta.scope, + }; + } + + async function getRefreshToken(refreshToken) { + const meta = db.findRefreshToken(refreshToken); + + if (!meta) { + return false; + } + if (meta.scope && !Array.isArray(meta.scope)) { + throw new Error("Scope should internally be an array"); + } + return { + refreshToken, + refreshTokenExpiresAt: meta.refreshTokenExpiresAt, + user: db.findUserById(meta.userId), + client: db.findClientById(meta.clientId), + scope: meta.scope, + }; + } + + async function revokeToken(token) { + db.deleteRefreshToken(token.refreshToken); + + return true; + } + + async function verifyScope(token, scope) { + if (!Array.isArray(scope)) { + throw new Error("Scope should internally be an array"); + } + return scope.every((s) => scopes.includes(s)); + } + + return Model.from({ + getUser, + getClient, + saveToken, + getAccessToken, + getRefreshToken, + revokeToken, + verifyScope, + }); } module.exports = createModel; diff --git a/test/helpers/request.js b/test/helpers/request.js index be556a8d..98e1c255 100644 --- a/test/helpers/request.js +++ b/test/helpers/request.js @@ -1,17 +1,17 @@ -const Request = require('../../lib/request'); +const Request = require("../../lib/request"); module.exports = (request) => { - const req = new Request({ - query: {}, - body: {}, - headers: {}, - method: 'GET', - ...request - }); + const req = new Request({ + query: {}, + body: {}, + headers: {}, + method: "GET", + ...request, + }); - req.is = function (header) { - return this.headers['content-type'] === header; - }; + req.is = function (header) { + return this.headers["content-type"] === header; + }; - return req; + return req; }; diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js index 202c7084..ec300859 100644 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -1,227 +1,308 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const Model = require('../../../lib/model'); -const Request = require('../../../lib/request'); -const InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); -const should = require('chai').should(); +const AbstractGrantType = require("../../../lib/grant-types/abstract-grant-type"); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const Model = require("../../../lib/model"); +const Request = require("../../../lib/request"); +const InvalidScopeError = require("../../../lib/errors/invalid-scope-error"); +const should = require("chai").should(); /** * Test `AbstractGrantType` integration. */ -describe('AbstractGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.accessTokenLifetime` is missing', function() { - try { - new AbstractGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessTokenLifetime`'); - } - }); - - it('should throw an error if `options.model` is missing', function() { - try { - new AbstractGrantType({ accessTokenLifetime: 123 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should set the `accessTokenLifetime`', function() { - const grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: {} }); - - grantType.accessTokenLifetime.should.equal(123); - }); - - it('should set the `model`', function() { - const model = Model.from({ async generateAccessToken () {} }); - const grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.model.should.equal(model); - }); - - it('should set the `refreshTokenLifetime`', function() { - const grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - grantType.refreshTokenLifetime.should.equal(456); - }); - }); - - describe('generateAccessToken()', function() { - it('should return an access token', async function() { - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - const accessToken = await handler.generateAccessToken(); - accessToken.should.be.a.sha256(); - }); - - it('should support promises', async function() { - const model = Model.from({ - generateAccessToken: async function() { - return 'long-hash-foo-bar'; - } - }); - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - const accessToken = await handler.generateAccessToken(); - accessToken.should.equal('long-hash-foo-bar'); - }); - - it('should support non-promises', async function() { - const model = Model.from({ - generateAccessToken: function() { - return 'long-hash-foo-bar'; - } - }); - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - const accessToken = await handler.generateAccessToken(); - accessToken.should.equal('long-hash-foo-bar'); - }); - }); - - describe('generateRefreshToken()', function() { - it('should return a refresh token', async function() { - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - const refreshToken = await handler.generateRefreshToken(); - refreshToken.should.be.a.sha256(); - }); - - it('should support promises', async function() { - const model = Model.from({ - generateRefreshToken: async function() { - return 'long-hash-foo-bar'; - } - }); - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - const refreshToken = await handler.generateRefreshToken(); - refreshToken.should.equal('long-hash-foo-bar'); - }); - - it('should support non-promises', async function() { - const model = Model.from({ - generateRefreshToken: function() { - return 'long-hash-foo-bar'; - } - }); - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 456 }); - const refreshToken = await handler.generateRefreshToken(); - refreshToken.should.equal('long-hash-foo-bar'); - }); - }); - - describe('getAccessTokenExpiresAt()', function() { - it('should return a date', function() { - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); - }); - }); - - describe('getRefreshTokenExpiresAt()', function() { - it('should return a refresh token', function() { - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - - handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); - }); - }); - - describe('getScope()', function() { - it('should throw an error if `scope` is invalid', function() { - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - const request = new Request({ body: { scope: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - handler.getScope(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal('Invalid parameter: `scope`'); - } - }); - - it('should allow the `scope` to be `undefined`', function() { - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - should.not.exist(handler.getScope(request)); - }); - - it('should return the scope', function() { - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - const request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - - handler.getScope(request).should.eql(['foo']); - }); - }); - - describe('validateScope()', function () { - it('accepts the scope, if the model does not implement it', async function () { - const scope = ['some,scope,this,that']; - const user = { id: 123 }; - const client = { id: 456 }; - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, refreshTokenLifetime: 456 }); - const validated = await handler.validateScope(user, client, scope); - validated.should.eql(scope); - }); - - it('accepts the scope, if the model accepts it', async function () { - const scope = ['some,scope,this,that']; - const user = { id: 123 }; - const client = { id: 456 }; - - const model = Model.from({ - async validateScope (_user, _client, _scope) { - // make sure the model received the correct args - _user.should.deep.equal(user); - _client.should.deep.equal(_client); - _scope.should.eql(scope); - - return scope; - } - }); - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, refreshTokenLifetime: 456 }); - const validated = await handler.validateScope(user, client, scope); - validated.should.eql(scope); - }); - - it('throws if the model rejects the scope', async function () { - const scope = ['some,scope,this,that']; - const user = { id: 123 }; - const client = { id: 456 }; - const returnTypes = [undefined, null, false, 0, '']; - - for (const type of returnTypes) { - const model = Model.from({ - async validateScope (_user, _client, _scope) { - // make sure the model received the correct args - _user.should.deep.equal(user); - _client.should.deep.equal(_client); - _scope.should.eql(scope); - - return type; - } - }); - const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, refreshTokenLifetime: 456 }); - - try { - await handler.validateScope(user, client, scope); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal('Invalid scope: Requested scope is invalid'); - } - } - }); - }); +describe("AbstractGrantType integration", function () { + describe("constructor()", function () { + it("should throw an error if `options.accessTokenLifetime` is missing", function () { + try { + new AbstractGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `accessTokenLifetime`"); + } + }); + + it("should throw an error if `options.model` is missing", function () { + try { + new AbstractGrantType({ accessTokenLifetime: 123 }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `model`"); + } + }); + + it("should set the `accessTokenLifetime`", function () { + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + }); + + grantType.accessTokenLifetime.should.equal(123); + }); + + it("should set the `model`", function () { + const model = Model.from({ async generateAccessToken() {} }); + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.model.should.equal(model); + }); + + it("should set the `refreshTokenLifetime`", function () { + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + grantType.refreshTokenLifetime.should.equal(456); + }); + }); + + describe("generateAccessToken()", function () { + it("should return an access token", async function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const accessToken = await handler.generateAccessToken(); + accessToken.should.be.a.sha256(); + }); + + it("should support promises", async function () { + const model = Model.from({ + generateAccessToken: async function () { + return "long-hash-foo-bar"; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 456, + }); + const accessToken = await handler.generateAccessToken(); + accessToken.should.equal("long-hash-foo-bar"); + }); + + it("should support non-promises", async function () { + const model = Model.from({ + generateAccessToken: function () { + return "long-hash-foo-bar"; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 456, + }); + const accessToken = await handler.generateAccessToken(); + accessToken.should.equal("long-hash-foo-bar"); + }); + }); + + describe("generateRefreshToken()", function () { + it("should return a refresh token", async function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.be.a.sha256(); + }); + + it("should support promises", async function () { + const model = Model.from({ + generateRefreshToken: async function () { + return "long-hash-foo-bar"; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 456, + }); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.equal("long-hash-foo-bar"); + }); + + it("should support non-promises", async function () { + const model = Model.from({ + generateRefreshToken: function () { + return "long-hash-foo-bar"; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 456, + }); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.equal("long-hash-foo-bar"); + }); + }); + + describe("getAccessTokenExpiresAt()", function () { + it("should return a date", function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); + }); + }); + + describe("getRefreshTokenExpiresAt()", function () { + it("should return a refresh token", function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); + }); + }); + + describe("getScope()", function () { + it("should throw an error if `scope` is invalid", function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: { scope: "øå€£‰" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + handler.getScope(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal("Invalid parameter: `scope`"); + } + }); + + it("should allow the `scope` to be `undefined`", function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + should.not.exist(handler.getScope(request)); + }); + + it("should return the scope", function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: { scope: "foo" }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getScope(request).should.eql(["foo"]); + }); + }); + + describe("validateScope()", function () { + it("accepts the scope, if the model does not implement it", async function () { + const scope = ["some,scope,this,that"]; + const user = { id: 123 }; + const client = { id: 456 }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const validated = await handler.validateScope(user, client, scope); + validated.should.eql(scope); + }); + + it("accepts the scope, if the model accepts it", async function () { + const scope = ["some,scope,this,that"]; + const user = { id: 123 }; + const client = { id: 456 }; + + const model = Model.from({ + async validateScope(_user, _client, _scope) { + // make sure the model received the correct args + _user.should.deep.equal(user); + _client.should.deep.equal(_client); + _scope.should.eql(scope); + + return scope; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + const validated = await handler.validateScope(user, client, scope); + validated.should.eql(scope); + }); + + it("throws if the model rejects the scope", async function () { + const scope = ["some,scope,this,that"]; + const user = { id: 123 }; + const client = { id: 456 }; + const returnTypes = [undefined, null, false, 0, ""]; + + for (const type of returnTypes) { + const model = Model.from({ + async validateScope(_user, _client, _scope) { + // make sure the model received the correct args + _user.should.deep.equal(user); + _client.should.deep.equal(_client); + _scope.should.eql(scope); + + return type; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + try { + await handler.validateScope(user, client, scope); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal("Invalid scope: Requested scope is invalid"); + } + } + }); + }); }); diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index c3ec4c09..0495e77f 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -1,627 +1,955 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AuthorizationCodeGrantType = require('../../../lib/grant-types/authorization-code-grant-type'); -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -const Model = require('../../../lib/model'); -const Request = require('../../../lib/request'); -const ServerError = require('../../../lib/errors/server-error'); -const should = require('chai').should(); +const AuthorizationCodeGrantType = require("../../../lib/grant-types/authorization-code-grant-type"); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); +const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); +const Model = require("../../../lib/model"); +const Request = require("../../../lib/request"); +const ServerError = require("../../../lib/errors/server-error"); +const should = require("chai").should(); /** * Test `AuthorizationCodeGrantType` integration. */ -describe('AuthorizationCodeGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new AuthorizationCodeGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getAuthorizationCode()`', function() { - try { - new AuthorizationCodeGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getAuthorizationCode()`'); - } - }); - - it('should throw an error if the model does not implement `revokeAuthorizationCode()`', function() { - try { - const model = Model.from({ - getAuthorizationCode: function() {} - }); - - new AuthorizationCodeGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `revokeAuthorizationCode()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - const model = Model.from({ - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {} - }); - - new AuthorizationCodeGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', async function() { - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - try { - await grantType.handle(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is invalid (not in code)', async function() { - const client = { id: 1234 }; - const model = Model.from({ - getAuthorizationCode: function(code) { - code.should.equal(123456789); - return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), user: {} }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 123456789 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.handle(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - } - }); - - it('should throw an error if `client` is missing', function() { - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - grantType.handle(request, null); - } - catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', async function() { - const client = { id: 'foobar' }; - const scope = ['fooscope']; - const user = { name: 'foouser' }; - const codeDoc = { - authorizationCode: 12345, - expiresAt: new Date(new Date() * 2), - client, - user, - scope - }; - const model = Model.from({ - getAuthorizationCode: async function (code) { - code.should.equal('code-1234'); - - return codeDoc; - }, - revokeAuthorizationCode: async function (_codeDoc) { - _codeDoc.should.deep.equal(codeDoc); - return true; - }, - validateScope: async function (_user, _client, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return scope; - }, - generateAccessToken: async function (_client, _user, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return 'long-access-token-hash'; - }, - generateRefreshToken: async function (_client, _user, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return 'long-refresh-token-hash'; - }, - saveToken: async function (_token, _client, _user) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _token.accessToken.should.equal('long-access-token-hash'); - _token.refreshToken.should.equal('long-refresh-token-hash'); - _token.authorizationCode.should.equal(codeDoc.authorizationCode); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return _token; - }, - }); - - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 'code-1234' }, headers: {}, method: {}, query: {} }); - - const token = await grantType.handle(request, client); - token.accessToken.should.equal('long-access-token-hash'); - token.refreshToken.should.equal('long-refresh-token-hash'); - token.authorizationCode.should.equal(codeDoc.authorizationCode); - token.accessTokenExpiresAt.should.be.instanceOf(Date); - token.refreshTokenExpiresAt.should.be.instanceOf(Date); - }); - - it('should support promises', function() { - const client = { id: 'foobar' }; - const model = Model.from({ - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const client = { id: 'foobar' }; - const model = Model.from({ - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('getAuthorizationCode()', function() { - it('should throw an error if the request body does not contain `code`', async function() { - const client = {}; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await grantType.getAuthorizationCode(request, client); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `code`'); - } - }); - - it('should throw an error if `code` is invalid', async function() { - const client = {}; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `code`'); - } - }); - - it('should throw an error if `authorizationCode` is missing', async function() { - const client = {}; - const model = Model.from({ - getAuthorizationCode: async function() {}, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - } - }); - - it('should throw an error if `authorizationCode.client` is missing', async function() { - const client = {}; - const model = Model.from({ - getAuthorizationCode: async function() { return { authorizationCode: 12345 }; }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); - } - }); - - it('should throw an error if `authorizationCode.expiresAt` is missing', async function() { - const client = {}; - const model = Model.from({ - getAuthorizationCode: async function() { - return { authorizationCode: 12345, client: {}, user: {} }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `expiresAt` must be a Date instance'); - } - }); - - it('should throw an error if `authorizationCode.user` is missing', async function() { - const client = {}; - const model = Model.from({ - getAuthorizationCode: async function() { - return { authorizationCode: 12345, client: {}, expiresAt: new Date() }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `user` object'); - } - }); - - it('should throw an error if the client id does not match', async function() { - const client = { id: 123 }; - const model = Model.from({ - getAuthorizationCode: async function() { - return { authorizationCode: 12345, expiresAt: new Date(), client: { id: 456 }, user: {} }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - } - }); - - it('should throw an error if the auth code is expired', async function() { - const client = { id: 123 }; - const date = new Date(new Date() / 2); - const model = Model.from({ - getAuthorizationCode: async function() { - return { authorizationCode: 12345, client: { id: 123 }, expiresAt: date, user: {} }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code has expired'); - } - }); - - it('should throw an error if the `redirectUri` is invalid (format)', async function() { - const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), redirectUri: 'foobar', user: {} }; - const client = { id: 'foobar' }; - const model = Model.from({ - getAuthorizationCode: async function() { return authorizationCode; }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: `redirect_uri` is not a valid URI'); - } - }); - - it('should return an auth code', async function() { - const authorizationCode = { - authorizationCode: 1234567, - client: { id: 'foobar' }, - expiresAt: new Date(new Date() * 2), user: {} - }; - const client = { id: 'foobar' }; - const model = Model.from({ - getAuthorizationCode: async function(_code) { - _code.should.equal(12345); - return authorizationCode; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - const code = await grantType.getAuthorizationCode(request, client); - code.should.deep.equal(authorizationCode); - }); - - it('should support promises', function() { - const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - const client = { id: 'foobar' }; - const model = Model.from({ - getAuthorizationCode: async function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; - const client = { id: 'foobar' }; - const model = Model.from({ - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('validateRedirectUri()', function() { - it('should throw an error if `redirectUri` is missing', function() { - const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - const model = Model.from({ - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return authorizationCode; }, - saveToken: function() {} - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - - try { - grantType.validateRedirectUri(request, authorizationCode); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); - } - }); - - it('should throw an error if `redirectUri` is invalid', function() { - const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - const model = Model.from({ - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345, redirect_uri: 'http://bar.foo' }, headers: {}, method: {}, query: {} }); - - try { - grantType.validateRedirectUri(request, authorizationCode); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is invalid'); - } - }); - it('returns undefined and does not throw if `redirectUri` is valid', async function () { - const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', user: {} }; - const model = Model.from({ - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() { return true; }, - saveToken: function() {} - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345, redirect_uri: 'http://foo.bar' }, headers: {}, method: {}, query: {} }); - const value = grantType.validateRedirectUri(request, authorizationCode); - const isUndefined = value === undefined; - isUndefined.should.equal(true); - }); - }); - - describe('revokeAuthorizationCode()', function() { - it('should revoke the auth code', async function() { - const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: async function(_code) { - _code.should.equal(authorizationCode); - return true; - }, - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - const data = await grantType.revokeAuthorizationCode(authorizationCode); - data.should.deep.equal(authorizationCode); - }); - - it('should throw an error when the auth code is invalid', async function() { - const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - const returnTypes = [false, null, undefined, 0, '']; - - for (const type of returnTypes) { - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: async function(_code) { - _code.should.equal(authorizationCode); - return type; - }, - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - try { - await grantType.revokeAuthorizationCode(authorizationCode); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: authorization code is invalid'); - } - } - }); - - it('should support promises', function() { - const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: async function() { return true; }, - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const authorizationCode = { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), user: {} }; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: function() { return authorizationCode; }, - saveToken: () => should.fail() - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', async function() { - const token = { foo: 'bar' }; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: function(_token, _client= 'fallback', _user= 'fallback') { - _token.accessToken.should.be.a.sha256(); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshToken.should.be.a.sha256(); - _token.scope.should.eql(['foo']); - (_token.authorizationCode === undefined).should.equal(true); - _user.should.equal('fallback'); - _client.should.equal('fallback'); - return token; - }, - validateScope: function(_user= 'fallback', _client= 'fallback', _scope = ['fallback']) { - _user.should.equal('fallback'); - _client.should.equal('fallback'); - _scope.should.eql(['fallback']); - return ['foo']; - } - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const data = await grantType.saveToken(); - data.should.equal(token); - }); - - it('should support promises', function() { - const token = {}; - const model = Model.from({ - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: async function() { return token; } - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const token = {}; - const model = Model.from({ - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: function() { return token; } - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); +describe("AuthorizationCodeGrantType integration", function () { + describe("constructor()", function () { + it("should throw an error if `model` is missing", function () { + try { + new AuthorizationCodeGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `model`"); + } + }); + + it("should throw an error if the model does not implement `getAuthorizationCode()`", function () { + try { + new AuthorizationCodeGrantType({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `getAuthorizationCode()`", + ); + } + }); + + it("should throw an error if the model does not implement `revokeAuthorizationCode()`", function () { + try { + const model = Model.from({ + getAuthorizationCode: function () {}, + }); + + new AuthorizationCodeGrantType({ model: model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `revokeAuthorizationCode()`", + ); + } + }); + + it("should throw an error if the model does not implement `saveToken()`", function () { + try { + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () {}, + }); + + new AuthorizationCodeGrantType({ model: model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `saveToken()`", + ); + } + }); + }); + + describe("handle()", function () { + it("should throw an error if `request` is missing", async function () { + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + try { + await grantType.handle(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `request`"); + } + }); + + it("should throw an error if `client` is invalid (not in code)", async function () { + const client = { id: 1234 }; + const model = Model.from({ + getAuthorizationCode: function (code) { + code.should.equal(123456789); + return { + authorizationCode: 12345, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 123456789 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.handle(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + "Server error: `getAuthorizationCode()` did not return a `client` object", + ); + } + }); + + it("should throw an error if `client` is missing", function () { + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + grantType.handle(request, null); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `client`"); + } + }); + + it("should return a token", async function () { + const client = { id: "foobar" }; + const scope = ["fooscope"]; + const user = { name: "foouser" }; + const codeDoc = { + authorizationCode: 12345, + expiresAt: new Date(new Date() * 2), + client, + user, + scope, + }; + const model = Model.from({ + getAuthorizationCode: async function (code) { + code.should.equal("code-1234"); + + return codeDoc; + }, + revokeAuthorizationCode: async function (_codeDoc) { + _codeDoc.should.deep.equal(codeDoc); + return true; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return "long-access-token-hash"; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return "long-refresh-token-hash"; + }, + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _token.accessToken.should.equal("long-access-token-hash"); + _token.refreshToken.should.equal("long-refresh-token-hash"); + _token.authorizationCode.should.equal(codeDoc.authorizationCode); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return _token; + }, + }); + + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: "code-1234" }, + headers: {}, + method: {}, + query: {}, + }); + + const token = await grantType.handle(request, client); + token.accessToken.should.equal("long-access-token-hash"); + token.refreshToken.should.equal("long-refresh-token-hash"); + token.authorizationCode.should.equal(codeDoc.authorizationCode); + token.accessTokenExpiresAt.should.be.instanceOf(Date); + token.refreshTokenExpiresAt.should.be.instanceOf(Date); + }); + + it("should support promises", function () { + const client = { id: "foobar" }; + const model = Model.from({ + getAuthorizationCode: function () { + return { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: function () { + return true; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const client = { id: "foobar" }; + const model = Model.from({ + getAuthorizationCode: function () { + return { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: function () { + return true; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + }); + + describe("getAuthorizationCode()", function () { + it("should throw an error if the request body does not contain `code`", async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Missing parameter: `code`"); + } + }); + + it("should throw an error if `code` is invalid", async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: "øå€£‰" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `code`"); + } + }); + + it("should throw an error if `authorizationCode` is missing", async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: async function () {}, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: authorization code is invalid"); + } + }); + + it("should throw an error if `authorizationCode.client` is missing", async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: async function () { + return { authorizationCode: 12345 }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + "Server error: `getAuthorizationCode()` did not return a `client` object", + ); + } + }); + + it("should throw an error if `authorizationCode.expiresAt` is missing", async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: async function () { + return { authorizationCode: 12345, client: {}, user: {} }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + "Server error: `expiresAt` must be a Date instance", + ); + } + }); + + it("should throw an error if `authorizationCode.user` is missing", async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: async function () { + return { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(), + }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + "Server error: `getAuthorizationCode()` did not return a `user` object", + ); + } + }); + + it("should throw an error if the client id does not match", async function () { + const client = { id: 123 }; + const model = Model.from({ + getAuthorizationCode: async function () { + return { + authorizationCode: 12345, + expiresAt: new Date(), + client: { id: 456 }, + user: {}, + }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: authorization code is invalid"); + } + }); + + it("should throw an error if the auth code is expired", async function () { + const client = { id: 123 }; + const date = new Date(new Date() / 2); + const model = Model.from({ + getAuthorizationCode: async function () { + return { + authorizationCode: 12345, + client: { id: 123 }, + expiresAt: date, + user: {}, + }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: authorization code has expired"); + } + }); + + it("should throw an error if the `redirectUri` is invalid (format)", async function () { + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date() * 2), + redirectUri: "foobar", + user: {}, + }; + const client = { id: "foobar" }; + const model = Model.from({ + getAuthorizationCode: async function () { + return authorizationCode; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + "Invalid grant: `redirect_uri` is not a valid URI", + ); + } + }); + + it("should return an auth code", async function () { + const authorizationCode = { + authorizationCode: 1234567, + client: { id: "foobar" }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + const client = { id: "foobar" }; + const model = Model.from({ + getAuthorizationCode: async function (_code) { + _code.should.equal(12345); + return authorizationCode; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + const code = await grantType.getAuthorizationCode(request, client); + code.should.deep.equal(authorizationCode); + }); + + it("should support promises", function () { + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + const client = { id: "foobar" }; + const model = Model.from({ + getAuthorizationCode: async function () { + return authorizationCode; + }, + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + grantType + .getAuthorizationCode(request, client) + .should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + const client = { id: "foobar" }; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + grantType + .getAuthorizationCode(request, client) + .should.be.an.instanceOf(Promise); + }); + }); + + describe("validateRedirectUri()", function () { + it("should throw an error if `redirectUri` is missing", function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + redirectUri: "http://foo.bar", + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + grantType.validateRedirectUri(request, authorizationCode); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: `redirect_uri` is not a valid URI", + ); + } + }); + + it("should throw an error if `redirectUri` is invalid", function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + redirectUri: "http://foo.bar", + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () { + return true; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345, redirect_uri: "http://bar.foo" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + grantType.validateRedirectUri(request, authorizationCode); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid request: `redirect_uri` is invalid"); + } + }); + it("returns undefined and does not throw if `redirectUri` is valid", async function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + redirectUri: "http://foo.bar", + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () { + return true; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345, redirect_uri: "http://foo.bar" }, + headers: {}, + method: {}, + query: {}, + }); + const value = grantType.validateRedirectUri(request, authorizationCode); + const isUndefined = value === undefined; + isUndefined.should.equal(true); + }); + }); + + describe("revokeAuthorizationCode()", function () { + it("should revoke the auth code", async function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function (_code) { + _code.should.equal(authorizationCode); + return true; + }, + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + const data = await grantType.revokeAuthorizationCode(authorizationCode); + data.should.deep.equal(authorizationCode); + }); + + it("should throw an error when the auth code is invalid", async function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + const returnTypes = [false, null, undefined, 0, ""]; + + for (const type of returnTypes) { + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function (_code) { + _code.should.equal(authorizationCode); + return type; + }, + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + try { + await grantType.revokeAuthorizationCode(authorizationCode); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + "Invalid grant: authorization code is invalid", + ); + } + } + }); + + it("should support promises", function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function () { + return true; + }, + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + grantType + .revokeAuthorizationCode(authorizationCode) + .should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: function () { + return authorizationCode; + }, + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + grantType + .revokeAuthorizationCode(authorizationCode) + .should.be.an.instanceOf(Promise); + }); + }); + + describe("saveToken()", function () { + it("should save the token", async function () { + const token = { foo: "bar" }; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: function (_token, _client = "fallback", _user = "fallback") { + _token.accessToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshToken.should.be.a.sha256(); + _token.scope.should.eql(["foo"]); + (_token.authorizationCode === undefined).should.equal(true); + _user.should.equal("fallback"); + _client.should.equal("fallback"); + return token; + }, + validateScope: function ( + _user = "fallback", + _client = "fallback", + _scope = ["fallback"], + ) { + _user.should.equal("fallback"); + _client.should.equal("fallback"); + _scope.should.eql(["fallback"]); + return ["foo"]; + }, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const data = await grantType.saveToken(); + data.should.equal(token); + }); + + it("should support promises", function () { + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () {}, + saveToken: async function () { + return token; + }, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () {}, + saveToken: function () { + return token; + }, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index 813a2081..72ae028f 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -1,251 +1,355 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -const Model = require('../../../lib/model'); -const Request = require('../../../lib/request'); -const should = require('chai').should(); +const ClientCredentialsGrantType = require("../../../lib/grant-types/client-credentials-grant-type"); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); +const Model = require("../../../lib/model"); +const Request = require("../../../lib/request"); +const should = require("chai").should(); /** * Test `ClientCredentialsGrantType` integration. */ -describe('ClientCredentialsGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new ClientCredentialsGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getUserFromClient()`', function() { - try { - new ClientCredentialsGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getUserFromClient()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - const model = Model.from({ - getUserFromClient: function() {} - }); - - new ClientCredentialsGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', async function() { - const model = Model.from({ - getUserFromClient: function() {}, - saveToken: function() {} - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - - try { - await grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is missing', async function() { - const model = Model.from({ - getUserFromClient: function() {}, - saveToken: function() {} - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await grantType.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', async function() { - const token = {}; - const client = { foo: 'bar' }; - const user = { name: 'foo' }; - const scope = ['fooscope']; - - const model = Model.from({ - getUserFromClient: async function(_client) { - _client.should.deep.equal(client); - return { ...user }; - }, - saveToken: async function(_token, _client, _user) { - _client.should.deep.equal(client); - _user.should.deep.equal(user); - _token.accessToken.should.equal('long-access-token-hash'); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.scope.should.eql(scope); - return token; - }, - validateScope: async function (_user, _client, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return scope; - }, - generateAccessToken: async function (_client, _user, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return 'long-access-token-hash'; - } - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: { scope: scope.join(' ') }, headers: {}, method: {}, query: {} }); - - const data = await grantType.handle(request, client); - data.should.equal(token); - }); - - it('should support promises', function() { - const token = {}; - const model = Model.from({ - getUserFromClient: async function() { return {}; }, - saveToken: async function() { return token; } - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.handle(request, {}).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const token = {}; - const model = Model.from({ - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; } - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.handle(request, {}).should.be.an.instanceOf(Promise); - }); - }); - - describe('getUserFromClient()', function() { - it('should throw an error if `user` is missing', function() { - const model = Model.from({ - getUserFromClient: function() {}, - saveToken: () => should.fail() - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - return grantType.getUserFromClient(request, {}) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); - }); - - it('should return a user', function() { - const user = { email: 'foo@bar.com' }; - const model = Model.from({ - getUserFromClient: function() { return user; }, - saveToken: () => should.fail() - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - return grantType.getUserFromClient(request, {}) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - const user = { email: 'foo@bar.com' }; - const model = Model.from({ - getUserFromClient: async function() { return user; }, - saveToken: () => should.fail() - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const user = { email: 'foo@bar.com' }; - const model = Model.from({ - getUserFromClient: function() {return user; }, - saveToken: () => should.fail() - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', async function() { - const token = {}; - const model = Model.from({ - getUserFromClient: () => should.fail(), - saveToken: function() { return token; }, - validateScope: function() { return ['foo']; } - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - const data = await grantType.saveToken(token); - data.should.equal(token); - }); - - it('should support promises', function() { - const token = {}; - const model = Model.from({ - getUserFromClient:() => should.fail(), - saveToken: async function() { return token; } - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const token = {}; - const model = Model.from({ - getUserFromClient: () => should.fail(), - saveToken: function() { return token; } - }); - const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, model: model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); +describe("ClientCredentialsGrantType integration", function () { + describe("constructor()", function () { + it("should throw an error if `model` is missing", function () { + try { + new ClientCredentialsGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `model`"); + } + }); + + it("should throw an error if the model does not implement `getUserFromClient()`", function () { + try { + new ClientCredentialsGrantType({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `getUserFromClient()`", + ); + } + }); + + it("should throw an error if the model does not implement `saveToken()`", function () { + try { + const model = Model.from({ + getUserFromClient: function () {}, + }); + + new ClientCredentialsGrantType({ model: model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `saveToken()`", + ); + } + }); + }); + + describe("handle()", function () { + it("should throw an error if `request` is missing", async function () { + const model = Model.from({ + getUserFromClient: function () {}, + saveToken: function () {}, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + + try { + await grantType.handle(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `request`"); + } + }); + + it("should throw an error if `client` is missing", async function () { + const model = Model.from({ + getUserFromClient: function () {}, + saveToken: function () {}, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `client`"); + } + }); + + it("should return a token", async function () { + const token = {}; + const client = { foo: "bar" }; + const user = { name: "foo" }; + const scope = ["fooscope"]; + + const model = Model.from({ + getUserFromClient: async function (_client) { + _client.should.deep.equal(client); + return { ...user }; + }, + saveToken: async function (_token, _client, _user) { + _client.should.deep.equal(client); + _user.should.deep.equal(user); + _token.accessToken.should.equal("long-access-token-hash"); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.scope.should.eql(scope); + return token; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return "long-access-token-hash"; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { scope: scope.join(" ") }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await grantType.handle(request, client); + data.should.equal(token); + }); + + it("should support promises", function () { + const token = {}; + const model = Model.from({ + getUserFromClient: async function () { + return {}; + }, + saveToken: async function () { + return token; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, {}).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const token = {}; + const model = Model.from({ + getUserFromClient: function () { + return {}; + }, + saveToken: function () { + return token; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, {}).should.be.an.instanceOf(Promise); + }); + }); + + describe("getUserFromClient()", function () { + it("should throw an error if `user` is missing", function () { + const model = Model.from({ + getUserFromClient: function () {}, + saveToken: () => should.fail(), + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + return grantType + .getUserFromClient(request, {}) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: user credentials are invalid"); + }); + }); + + it("should return a user", function () { + const user = { email: "foo@bar.com" }; + const model = Model.from({ + getUserFromClient: function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + return grantType + .getUserFromClient(request, {}) + .then(function (data) { + data.should.equal(user); + }) + .catch(should.fail); + }); + + it("should support promises", function () { + const user = { email: "foo@bar.com" }; + const model = Model.from({ + getUserFromClient: async function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const user = { email: "foo@bar.com" }; + const model = Model.from({ + getUserFromClient: function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); + }); + }); + + describe("saveToken()", function () { + it("should save the token", async function () { + const token = {}; + const model = Model.from({ + getUserFromClient: () => should.fail(), + saveToken: function () { + return token; + }, + validateScope: function () { + return ["foo"]; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const data = await grantType.saveToken(token); + data.should.equal(token); + }); + + it("should support promises", function () { + const token = {}; + const model = Model.from({ + getUserFromClient: () => should.fail(), + saveToken: async function () { + return token; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const token = {}; + const model = Model.from({ + getUserFromClient: () => should.fail(), + saveToken: function () { + return token; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index 25da266b..58757686 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -1,363 +1,490 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -const PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); -const Model = require('../../../lib/model'); -const Request = require('../../../lib/request'); -const should = require('chai').should(); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); +const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); +const PasswordGrantType = require("../../../lib/grant-types/password-grant-type"); +const Model = require("../../../lib/model"); +const Request = require("../../../lib/request"); +const should = require("chai").should(); /** * Test `PasswordGrantType` integration. */ -describe('PasswordGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new PasswordGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getUser()`', function() { - try { - new PasswordGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getUser()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - const model = Model.from({ - getUser: function() {} - }); - - new PasswordGrantType({ model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', async function() { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - - try { - await grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is missing', async function() { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - - try { - await grantType.handle({}); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', async function() { - const client = { id: 'foobar' }; - const scope = ['baz']; - const token = {}; - const user = { - id: 123456, - username: 'foo', - email: 'foo@example.com' - }; - - const model = Model.from({ - getUser: async function(username, password) { - username.should.equal('foo'); - password.should.equal('bar'); - return user; - }, - validateScope: async function(_user, _client, _scope) { - _client.should.equal(client); - _user.should.equal(user); - _scope.should.eql(scope); - return scope; - }, - generateAccessToken: async function (_client, _user, _scope) { - _client.should.equal(client); - _user.should.equal(user); - _scope.should.eql(scope); - return 'long-access-token-hash'; - }, - generateRefreshToken: async function (_client, _user, _scope) { - _client.should.equal(client); - _user.should.equal(user); - _scope.should.eql(scope); - return 'long-refresh-token-hash'; - }, - saveToken: async function(_token, _client, _user) { - _client.should.equal(client); - _user.should.equal(user); - _token.accessToken.should.equal('long-access-token-hash'); - _token.refreshToken.should.equal('long-refresh-token-hash'); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return token; - } - }); - - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: 'foo', password: 'bar', scope: 'baz' }, headers: {}, method: {}, query: {} }); - - const data = await grantType.handle(request, client); - data.should.equal(token); - }); - - it('should support promises', async function() { - const client = { id: 'foobar' }; - const token = {}; - const model = Model.from({ - getUser: async function() { return {}; }, - saveToken: async function() { return token; } - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - const result = await grantType.handle(request, client); - result.should.deep.equal({}); - }); - - it('should support non-promises', async function() { - const client = { id: 'foobar' }; - const token = {}; - const model = Model.from({ - getUser: function() { return {}; }, - saveToken: function() { return token; } - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - const result = await grantType.handle(request, client); - result.should.deep.equal({}); - }); - }); - - describe('getUser()', function() { - it('should throw an error if the request body does not contain `username`', async function() { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail() - }); - const client = { id: 'foobar' }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await grantType.getUser(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `username`'); - } - }); - - it('should throw an error if the request body does not contain `password`', async function() { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail() - }); - const client = { id: 'foobar' }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getUser(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `password`'); - } - }); - - it('should throw an error if `username` is invalid', async function() { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail() - }); - const client = { id: 'foobar' }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: '\r\n', password: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getUser(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `username`'); - } - }); - - it('should throw an error if `password` is invalid', async function() { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail() - }); - const client = { id: 'foobar' }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: 'foobar', password: '\r\n' }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getUser(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `password`'); - } - }); - - it('should throw an error if `user` is missing', async function() { - const model = Model.from({ - getUser: async () => undefined, - saveToken: () => should.fail() - }); - const client = { id: 'foobar' }; - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getUser(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - } - }); - - it('should return a user', async function() { - const user = { email: 'foo@bar.com' }; - const client = { id: 'foobar' }; - const model = Model.from({ - getUser: function(username, password) { - username.should.equal('foo'); - password.should.equal('bar'); - return user; - }, - saveToken: () => should.fail() - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - const data = await grantType.getUser(request, client); - data.should.equal(user); - }); - - it('should support promises', function() { - const user = { email: 'foo@bar.com' }; - const client = { id: 'foobar' }; - const model = Model.from({ - getUser: async function() { return user; }, - saveToken: () => should.fail() - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.getUser(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const user = { email: 'foo@bar.com' }; - const client = { id: 'foobar' }; - const model = Model.from({ - getUser: function() { return user; }, - saveToken: () => should.fail() - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - grantType.getUser(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', async function() { - const token = {}; - const model = Model.from({ - getUser: () => should.fail(), - saveToken: async function(_token, _client = 'fallback', _user = 'fallback') { - _token.accessToken.should.be.a.sha256(); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshToken.should.be.a.sha256(); - _token.scope.should.eql(['foo']); - _client.should.equal('fallback'); - _user.should.equal('fallback'); - return token; - }, - validateScope: async function(_scope = ['fallback']) { - _scope.should.eql(['fallback']); - return ['foo']; - } - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - - const data = await grantType.saveToken(); - data.should.equal(token); - }); - - it('should support promises', function() { - const token = {}; - const model = Model.from({ - getUser: () => should.fail(), - saveToken: async function() { return token; } - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const token = {}; - const model = Model.from({ - getUser: () => should.fail(), - saveToken: function() { return token; } - }); - const grantType = new PasswordGrantType({ accessTokenLifetime: 123, model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); +describe("PasswordGrantType integration", function () { + describe("constructor()", function () { + it("should throw an error if `model` is missing", function () { + try { + new PasswordGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `model`"); + } + }); + + it("should throw an error if the model does not implement `getUser()`", function () { + try { + new PasswordGrantType({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `getUser()`", + ); + } + }); + + it("should throw an error if the model does not implement `saveToken()`", function () { + try { + const model = Model.from({ + getUser: function () {}, + }); + + new PasswordGrantType({ model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `saveToken()`", + ); + } + }); + }); + + describe("handle()", function () { + it("should throw an error if `request` is missing", async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `request`"); + } + }); + + it("should throw an error if `client` is missing", async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle({}); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `client`"); + } + }); + + it("should return a token", async function () { + const client = { id: "foobar" }; + const scope = ["baz"]; + const token = {}; + const user = { + id: 123456, + username: "foo", + email: "foo@example.com", + }; + + const model = Model.from({ + getUser: async function (username, password) { + username.should.equal("foo"); + password.should.equal("bar"); + return user; + }, + validateScope: async function (_user, _client, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.eql(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.eql(scope); + return "long-access-token-hash"; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.eql(scope); + return "long-refresh-token-hash"; + }, + saveToken: async function (_token, _client, _user) { + _client.should.equal(client); + _user.should.equal(user); + _token.accessToken.should.equal("long-access-token-hash"); + _token.refreshToken.should.equal("long-refresh-token-hash"); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + }, + }); + + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "foo", password: "bar", scope: "baz" }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await grantType.handle(request, client); + data.should.equal(token); + }); + + it("should support promises", async function () { + const client = { id: "foobar" }; + const token = {}; + const model = Model.from({ + getUser: async function () { + return {}; + }, + saveToken: async function () { + return token; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "foo", password: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + + const result = await grantType.handle(request, client); + result.should.deep.equal({}); + }); + + it("should support non-promises", async function () { + const client = { id: "foobar" }; + const token = {}; + const model = Model.from({ + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "foo", password: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + + const result = await grantType.handle(request, client); + result.should.deep.equal({}); + }); + }); + + describe("getUser()", function () { + it("should throw an error if the request body does not contain `username`", async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const client = { id: "foobar" }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Missing parameter: `username`"); + } + }); + + it("should throw an error if the request body does not contain `password`", async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const client = { id: "foobar" }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "foo" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Missing parameter: `password`"); + } + }); + + it("should throw an error if `username` is invalid", async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const client = { id: "foobar" }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "\r\n", password: "foobar" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `username`"); + } + }); + + it("should throw an error if `password` is invalid", async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const client = { id: "foobar" }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "foobar", password: "\r\n" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `password`"); + } + }); + + it("should throw an error if `user` is missing", async function () { + const model = Model.from({ + getUser: async () => undefined, + saveToken: () => should.fail(), + }); + const client = { id: "foobar" }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "foo", password: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: user credentials are invalid"); + } + }); + + it("should return a user", async function () { + const user = { email: "foo@bar.com" }; + const client = { id: "foobar" }; + const model = Model.from({ + getUser: function (username, password) { + username.should.equal("foo"); + password.should.equal("bar"); + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "foo", password: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await grantType.getUser(request, client); + data.should.equal(user); + }); + + it("should support promises", function () { + const user = { email: "foo@bar.com" }; + const client = { id: "foobar" }; + const model = Model.from({ + getUser: async function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "foo", password: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.getUser(request, client).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const user = { email: "foo@bar.com" }; + const client = { id: "foobar" }; + const model = Model.from({ + getUser: function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: "foo", password: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.getUser(request, client).should.be.an.instanceOf(Promise); + }); + }); + + describe("saveToken()", function () { + it("should save the token", async function () { + const token = {}; + const model = Model.from({ + getUser: () => should.fail(), + saveToken: async function ( + _token, + _client = "fallback", + _user = "fallback", + ) { + _token.accessToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshToken.should.be.a.sha256(); + _token.scope.should.eql(["foo"]); + _client.should.equal("fallback"); + _user.should.equal("fallback"); + return token; + }, + validateScope: async function (_scope = ["fallback"]) { + _scope.should.eql(["fallback"]); + return ["foo"]; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + const data = await grantType.saveToken(); + data.should.equal(token); + }); + + it("should support promises", function () { + const token = {}; + const model = Model.from({ + getUser: () => should.fail(), + saveToken: async function () { + return token; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const token = {}; + const model = Model.from({ + getUser: () => should.fail(), + saveToken: function () { + return token; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index 658983aa..188e38fe 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -1,552 +1,791 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -const RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); -const Model = require('../../../lib/model'); -const Request = require('../../../lib/request'); -const ServerError = require('../../../lib/errors/server-error'); -const should = require('chai').should(); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); +const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); +const RefreshTokenGrantType = require("../../../lib/grant-types/refresh-token-grant-type"); +const Model = require("../../../lib/model"); +const Request = require("../../../lib/request"); +const ServerError = require("../../../lib/errors/server-error"); +const should = require("chai").should(); /** * Test `RefreshTokenGrantType` integration. */ -describe('RefreshTokenGrantType integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - try { - new RefreshTokenGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getRefreshToken()`', function() { - try { - new RefreshTokenGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getRefreshToken()`'); - } - }); - - it('should throw an error if the model does not implement `revokeToken()`', function() { - try { - const model = Model.from({ - getRefreshToken: () => should.fail() - }); - - new RefreshTokenGrantType({ model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `revokeToken()`'); - } - }); - - it('should throw an error if the model does not implement `saveToken()`', function() { - try { - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail() - }); - - new RefreshTokenGrantType({ model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); - } - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', async function() { - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - - try { - await grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `request`'); - } - }); - - it('should throw an error if `client` is missing', async function() { - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await grantType.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - - it('should return a token', async function() { - const client = { id: 123 }; - const token = { - accessToken: 'foo', - client: { id: 123 }, - user: { name: 'foo' }, - scope: ['read', 'write'], - refreshTokenExpiresAt: new Date( new Date() * 2) - }; - const model = Model.from({ - getRefreshToken: async function(_refreshToken) { - _refreshToken.should.equal('foobar_refresh'); - return token; - }, - revokeToken: async function(_token) { - _token.should.deep.equal(token); - return true; - }, - generateAccessToken: async function (_client, _user, _scope) { - _user.should.deep.equal({ name: 'foo' }); - _client.should.deep.equal({ id: 123 }); - _scope.should.eql(['read', 'write']); - return 'new-access-token'; - }, - generateRefreshToken: async function (_client, _user, _scope) { - _user.should.deep.equal({ name: 'foo' }); - _client.should.deep.equal({ id: 123 }); - _scope.should.eql(['read', 'write']); - return 'new-refresh-token'; - }, - saveToken: async function(_token, _client, _user) { - _user.should.deep.equal({ name: 'foo' }); - _client.should.deep.equal({ id: 123 }); - _token.accessToken.should.equal('new-access-token'); - _token.refreshToken.should.equal('new-refresh-token'); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return token; - } - }); - - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, query: {} }); - const data = await grantType.handle(request, client); - data.should.equal(token); - }); - - it('should support promises', function() { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, - revokeToken: async function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: async function() { return { accessToken: 'foo', client: {}, user: {} }; } - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function() { return { accessToken: 'foo', client: { id: 123 }, user: {} }; }, - revokeToken: async function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; }, - saveToken: async function() { return { accessToken: 'foo', client: {}, user: {} }; } - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('getRefreshToken()', function() { - it('should throw an error if the `refreshToken` parameter is missing from the request body', async function() { - const client = {}; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `refresh_token`'); - } - }); - - it('should throw an error if `refreshToken` is not found', async function() { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function() {} , - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: { refresh_token: '12345' }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid'); - } - }); - - it('should throw an error if `refreshToken.client` is missing', async function() { - const client = {}; - const model = Model.from({ - getRefreshToken: async function() { return {}; }, - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `client` object'); - } - }); - - it('should throw an error if `refreshToken.user` is missing', async function() { - const client = {}; - const model = Model.from({ - getRefreshToken: async function() { - return { accessToken: 'foo', client: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getRefreshToken()` did not return a `user` object'); - } - }); - - it('should throw an error if the client id does not match', async function() { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function() { - return { accessToken: 'foo', client: { id: 456 }, user: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token was issued to another client'); - } - }); - - it('should throw an error if `refresh_token` contains invalid characters', async function() { - const client = {}; - const model = Model.from({ - getRefreshToken: async function() { - return { client: { id: 456 }, user: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: { refresh_token: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `refresh_token`'); - } - }); - - it('should throw an error if `refresh_token` is missing', async function() { - const client = {}; - const model = Model.from({ - getRefreshToken: async function() { - return { accessToken: 'foo', client: { id: 456 }, user: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token was issued to another client'); - } - }); - - it('should throw an error if `refresh_token` is expired', async function() { - const client = { id: 123 }; - const date = new Date(new Date() / 2); - const model = Model.from({ - getRefreshToken: async function() { - return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: date, user: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token has expired'); - } - }); - - it('should throw an error if `refreshTokenExpiresAt` is not a date value', async function() { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function() { - return { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: 'stringvalue', user: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, query: {} }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `refreshTokenExpiresAt` must be a Date instance'); - } - }); - - it('should return a token', async function() { - const client = { id: 123 }; - const token = { accessToken: 'foo', client: { id: 123 }, user: { name: 'foobar' } }; - const model = Model.from({ - getRefreshToken: async function(_refreshToken) { - _refreshToken.should.equal('foobar_refresh'); - return token; - }, - revokeToken: async function(_token) { - _token.should.deep.equal(token); - return true; - }, - saveToken: async function(_token, _client, _user) { - _user.should.deep.equal(token.user); - _client.should.deep.equal(client); - _token.accessToken.should.be.a.sha256(); - _token.refreshToken.should.be.a.sha256(); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return token; - } - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, query: {} }); - - const data = await grantType.getRefreshToken(request, client); - data.should.equal(token); - }); - - it('should support promises', function() { - const client = { id: 123 }; - const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - const model = Model.from({ - getRefreshToken: async function() { return token; }, - revokeToken: async function() {}, - saveToken: async function() {} - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const client = { id: 123 }; - const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; - const model = Model.from({ - getRefreshToken: async function() { return token; }, - revokeToken: async function() {}, - saveToken: async function() {} - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, query: {} }); - - grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe('revokeToken()', function() { - it('should throw an error if the `token` is invalid', async function() { - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: async () => {}, - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, model }); - - try { - await grantType.revokeToken({}); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: refresh token is invalid or could not be revoked'); - } - }); - - it('should revoke the token', async function() { - const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: async function(_token) { - _token.should.deep.equal(token); - return token; - }, - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - - const data = await grantType.revokeToken(token); - data.should.equal(token); - }); - - it('should support promises', function() { - const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: async function() { return token; }, - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const token = { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: function() { return token; }, - saveToken: () => should.fail() - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - }); - - describe('saveToken()', function() { - it('should save the token', async function() { - const user = { name: 'foo' }; - const client = { id: 123465 }; - const scope = ['foo', 'bar']; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: async function(_token, _client, _user) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _token.scope.should.deep.eql(scope); - _token.accessToken.should.be.a.sha256(); - _token.refreshToken.should.be.a.sha256(); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return { ..._token }; - } - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - - const data = await grantType.saveToken(user, client, scope); - data.accessToken.should.be.a.sha256(); - data.refreshToken.should.be.a.sha256(); - data.accessTokenExpiresAt.should.be.instanceOf(Date); - data.refreshTokenExpiresAt.should.be.instanceOf(Date); - data.scope.should.deep.equal(scope); - }); - - it('should support promises', function() { - const token = {}; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: async function() { return token; } - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const token = {}; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: function() { return token; } - }); - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); +describe("RefreshTokenGrantType integration", function () { + describe("constructor()", function () { + it("should throw an error if `model` is missing", function () { + try { + new RefreshTokenGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `model`"); + } + }); + + it("should throw an error if the model does not implement `getRefreshToken()`", function () { + try { + new RefreshTokenGrantType({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `getRefreshToken()`", + ); + } + }); + + it("should throw an error if the model does not implement `revokeToken()`", function () { + try { + const model = Model.from({ + getRefreshToken: () => should.fail(), + }); + + new RefreshTokenGrantType({ model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `revokeToken()`", + ); + } + }); + + it("should throw an error if the model does not implement `saveToken()`", function () { + try { + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + }); + + new RefreshTokenGrantType({ model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `saveToken()`", + ); + } + }); + }); + + describe("handle()", function () { + it("should throw an error if `request` is missing", async function () { + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + try { + await grantType.handle(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `request`"); + } + }); + + it("should throw an error if `client` is missing", async function () { + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `client`"); + } + }); + + it("should return a token", async function () { + const client = { id: 123 }; + const token = { + accessToken: "foo", + client: { id: 123 }, + user: { name: "foo" }, + scope: ["read", "write"], + refreshTokenExpiresAt: new Date(new Date() * 2), + }; + const model = Model.from({ + getRefreshToken: async function (_refreshToken) { + _refreshToken.should.equal("foobar_refresh"); + return token; + }, + revokeToken: async function (_token) { + _token.should.deep.equal(token); + return true; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal({ name: "foo" }); + _client.should.deep.equal({ id: 123 }); + _scope.should.eql(["read", "write"]); + return "new-access-token"; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _user.should.deep.equal({ name: "foo" }); + _client.should.deep.equal({ id: 123 }); + _scope.should.eql(["read", "write"]); + return "new-refresh-token"; + }, + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal({ name: "foo" }); + _client.should.deep.equal({ id: 123 }); + _token.accessToken.should.equal("new-access-token"); + _token.refreshToken.should.equal("new-refresh-token"); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + }, + }); + + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: "foobar_refresh" }, + headers: {}, + method: {}, + query: {}, + }); + const data = await grantType.handle(request, client); + data.should.equal(token); + }); + + it("should support promises", function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: "foo", client: { id: 123 }, user: {} }; + }, + revokeToken: async function () { + return { + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + saveToken: async function () { + return { accessToken: "foo", client: {}, user: {} }; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: "foobar" }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: "foo", client: { id: 123 }, user: {} }; + }, + revokeToken: async function () { + return { + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + saveToken: async function () { + return { accessToken: "foo", client: {}, user: {} }; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: "foobar" }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + }); + + describe("getRefreshToken()", function () { + it("should throw an error if the `refreshToken` parameter is missing from the request body", async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Missing parameter: `refresh_token`"); + } + }); + + it("should throw an error if `refreshToken` is not found", async function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () {}, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: "12345" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: refresh token is invalid"); + } + }); + + it("should throw an error if `refreshToken.client` is missing", async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: async function () { + return {}; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + "Server error: `getRefreshToken()` did not return a `client` object", + ); + } + }); + + it("should throw an error if `refreshToken.user` is missing", async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: "foo", client: {} }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + "Server error: `getRefreshToken()` did not return a `user` object", + ); + } + }); + + it("should throw an error if the client id does not match", async function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: "foo", client: { id: 456 }, user: {} }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + "Invalid grant: refresh token was issued to another client", + ); + } + }); + + it("should throw an error if `refresh_token` contains invalid characters", async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: async function () { + return { client: { id: 456 }, user: {} }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: "øå€£‰" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `refresh_token`"); + } + }); + + it("should throw an error if `refresh_token` is missing", async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: "foo", client: { id: 456 }, user: {} }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + "Invalid grant: refresh token was issued to another client", + ); + } + }); + + it("should throw an error if `refresh_token` is expired", async function () { + const client = { id: 123 }; + const date = new Date(new Date() / 2); + const model = Model.from({ + getRefreshToken: async function () { + return { + accessToken: "foo", + client: { id: 123 }, + refreshTokenExpiresAt: date, + user: {}, + }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: refresh token has expired"); + } + }); + + it("should throw an error if `refreshTokenExpiresAt` is not a date value", async function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () { + return { + accessToken: "foo", + client: { id: 123 }, + refreshTokenExpiresAt: "stringvalue", + user: {}, + }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + "Server error: `refreshTokenExpiresAt` must be a Date instance", + ); + } + }); + + it("should return a token", async function () { + const client = { id: 123 }; + const token = { + accessToken: "foo", + client: { id: 123 }, + user: { name: "foobar" }, + }; + const model = Model.from({ + getRefreshToken: async function (_refreshToken) { + _refreshToken.should.equal("foobar_refresh"); + return token; + }, + revokeToken: async function (_token) { + _token.should.deep.equal(token); + return true; + }, + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal(token.user); + _client.should.deep.equal(client); + _token.accessToken.should.be.a.sha256(); + _token.refreshToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: "foobar_refresh" }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await grantType.getRefreshToken(request, client); + data.should.equal(token); + }); + + it("should support promises", function () { + const client = { id: 123 }; + const token = { accessToken: "foo", client: { id: 123 }, user: {} }; + const model = Model.from({ + getRefreshToken: async function () { + return token; + }, + revokeToken: async function () {}, + saveToken: async function () {}, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: "foobar" }, + headers: {}, + method: {}, + query: {}, + }); + + grantType + .getRefreshToken(request, client) + .should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const client = { id: 123 }; + const token = { accessToken: "foo", client: { id: 123 }, user: {} }; + const model = Model.from({ + getRefreshToken: async function () { + return token; + }, + revokeToken: async function () {}, + saveToken: async function () {}, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: "foobar" }, + headers: {}, + method: {}, + query: {}, + }); + + grantType + .getRefreshToken(request, client) + .should.be.an.instanceOf(Promise); + }); + }); + + describe("revokeToken()", function () { + it("should throw an error if the `token` is invalid", async function () { + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: async () => {}, + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + try { + await grantType.revokeToken({}); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + "Invalid grant: refresh token is invalid or could not be revoked", + ); + } + }); + + it("should revoke the token", async function () { + const token = { + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: async function (_token) { + _token.should.deep.equal(token); + return token; + }, + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + const data = await grantType.revokeToken(token); + data.should.equal(token); + }); + + it("should support promises", function () { + const token = { + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: async function () { + return token; + }, + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const token = { + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: function () { + return token; + }, + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); + }); + + describe("saveToken()", function () { + it("should save the token", async function () { + const user = { name: "foo" }; + const client = { id: 123465 }; + const scope = ["foo", "bar"]; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _token.scope.should.deep.eql(scope); + _token.accessToken.should.be.a.sha256(); + _token.refreshToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return { ..._token }; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + const data = await grantType.saveToken(user, client, scope); + data.accessToken.should.be.a.sha256(); + data.refreshToken.should.be.a.sha256(); + data.accessTokenExpiresAt.should.be.instanceOf(Date); + data.refreshTokenExpiresAt.should.be.instanceOf(Date); + data.scope.should.deep.equal(scope); + }); + + it("should support promises", function () { + const token = {}; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: async function () { + return token; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const token = {}; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: function () { + return token; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index c1524f47..6d7728f1 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -1,710 +1,906 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AccessDeniedError = require('../../../lib/errors/access-denied-error'); -const AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -const InsufficientScopeError = require('../../../lib/errors/insufficient-scope-error'); -const InvalidTokenError = require('../../../lib/errors/invalid-token-error'); -const Model = require('../../../lib/model'); -const Request = require('../../../lib/request'); -const Response = require('../../../lib/response'); -const ServerError = require('../../../lib/errors/server-error'); -const UnauthorizedRequestError = require('../../../lib/errors/unauthorized-request-error'); -const should = require('chai').should(); +const AccessDeniedError = require("../../../lib/errors/access-denied-error"); +const AuthenticateHandler = require("../../../lib/handlers/authenticate-handler"); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); +const InsufficientScopeError = require("../../../lib/errors/insufficient-scope-error"); +const InvalidTokenError = require("../../../lib/errors/invalid-token-error"); +const Model = require("../../../lib/model"); +const Request = require("../../../lib/request"); +const Response = require("../../../lib/response"); +const ServerError = require("../../../lib/errors/server-error"); +const UnauthorizedRequestError = require("../../../lib/errors/unauthorized-request-error"); +const should = require("chai").should(); /** * Test `AuthenticateHandler` integration. */ -describe('AuthenticateHandler integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.model` is missing', function() { - try { - new AuthenticateHandler(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getAccessToken()`', function() { - try { - new AuthenticateHandler({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); - } - }); - - it('should throw an error if `scope` was given and `addAcceptedScopesHeader()` is missing', function() { - try { - new AuthenticateHandler({ model: { getAccessToken: function() {} }, scope: ['foobar'] }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `addAcceptedScopesHeader`'); - } - }); - - it('should throw an error if `scope` was given and `addAuthorizedScopesHeader()` is missing', function() { - try { - new AuthenticateHandler({ addAcceptedScopesHeader: true, model: { getAccessToken: function() {} }, scope: ['foobar'] }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `addAuthorizedScopesHeader`'); - } - }); - - it('should throw an error if `scope` was given and the model does not implement `verifyScope()`', function() { - try { - new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: { getAccessToken: function() {} }, scope: ['foobar'] }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `verifyScope()`'); - } - }); - - it('should set the `model`', function() { - const model = Model.from({ getAccessToken: function() {} }); - const grantType = new AuthenticateHandler({ model: model }); - - grantType.model.should.equal(model); - }); - - it('should set the `scope`', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() {} - }); - const grantType = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: 'foobar' - }); - - grantType.scope.should.eql(['foobar']); - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing or not a Request instance', async function() { - class Request {} // intentionally fake - const values = [undefined, null, {}, [], new Date(), new Request()]; - for (const request of values) { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - try { - await handler.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); - } - } - }); - - it('should throw an error if `response` is missing or not a Response instance', async function() { - class Response {} // intentionally fake - const values = [undefined, null, {}, [], new Date(), new Response()]; - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - - for (const response of values) { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - try { - await handler.handle(request, response); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `response` must be an instance of Response'); - } - } - }); - - it('should set the `WWW-Authenticate` header if an unauthorized request error is thrown', async function() { - const model = Model.from({ - getAccessToken: function() { - throw new UnauthorizedRequestError(); - } - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - response.get('WWW-Authenticate').should.equal('Bearer realm="Service"'); - } - }); - - it('should set the `WWW-Authenticate` header if an InvalidRequestError is thrown', function() { - const model = Model.from({ - getAccessToken: function() { - throw new InvalidRequestError(); - } - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('WWW-Authenticate').should.equal('Bearer realm="Service",error="invalid_request"'); - }); - }); - - it('should set the `WWW-Authenticate` header if an InvalidTokenError is thrown', function() { - const model = Model.from({ - getAccessToken: function() { - throw new InvalidTokenError(); - } - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('WWW-Authenticate').should.equal('Bearer realm="Service",error="invalid_token"'); - }); - }); - - it('should set the `WWW-Authenticate` header if an InsufficientScopeError is thrown', function() { - const model = Model.from({ - getAccessToken: function() { - throw new InsufficientScopeError(); - } - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.get('WWW-Authenticate').should.equal('Bearer realm="Service",error="insufficient_scope"'); - }); - }); - - it('should throw the error if an oauth error is thrown', function() { - const model = Model.from({ - getAccessToken: function() { - throw new AccessDeniedError('Cannot request this access token'); - } - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal('Cannot request this access token'); - }); - }); - - it('should throw a server error if a non-oauth error is thrown', function() { - const model = Model.from({ - getAccessToken: function() { - throw new Error('Unhandled exception'); - } - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Unhandled exception'); - }); - }); - - it('should return an access token', function() { - const accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - const model = Model.from({ - getAccessToken: function() { - return accessToken; - }, - verifyScope: function() { - return true; - } - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); - const request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: {} - }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - - it('should return an access token (deprecated)', function() { - const accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - const model = Model.from({ - getAccessToken: function() { - return accessToken; - }, - verifyScope: function() { - return true; - } - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - const request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: {} - }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - }); - - describe('getTokenFromRequest()', function() { - it('should throw an error if more than one authentication method is used', async function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: { access_token: 'foo' } - }); - - try { - await handler.getTokenFromRequest(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: only one authentication method is allowed'); - } - }); - - it('should throw an error if `accessToken` is missing', async function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await handler.getTokenFromRequest(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnauthorizedRequestError); - e.message.should.equal('Unauthorized request: no authentication given'); - } - }); - }); - - describe('getTokenFromRequestHeader()', function() { - it('should throw an error if the token is malformed', async function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ - body: {}, - headers: { - 'Authorization': 'foobar' - }, - method: {}, - query: {} - }); - - try { - await handler.getTokenFromRequestHeader(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: malformed authorization header'); - } - }); - - it('should return the bearer token', function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ - body: {}, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: {} - }); - - const bearerToken = handler.getTokenFromRequestHeader(request); - - bearerToken.should.equal('foo'); - }); - }); - - describe('getTokenFromRequestQuery()', function() { - it('should throw an error if the query contains a token', async function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - try { - await handler.getTokenFromRequestQuery(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: do not send bearer tokens in query URLs'); - } - }); - - it('should return the bearer token if `allowBearerTokensInQueryString` is true', function() { - const handler = new AuthenticateHandler({ allowBearerTokensInQueryString: true, model: { getAccessToken: function() {} } }); - - handler.getTokenFromRequestQuery({ query: { access_token: 'foo' } }).should.equal('foo'); - }); - }); - - describe('getTokenFromRequestBody()', function() { - it('should throw an error if the method is `GET`', async function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ - body: { access_token: 'foo' }, - headers: {}, - method: 'GET', - query: {} - }); - - try { - await handler.getTokenFromRequestBody(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: token may not be passed in the body when using the GET verb'); - } - }); - - it('should throw an error if the media type is not `application/x-www-form-urlencoded`', async function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ - body: { access_token: 'foo' }, - headers: {}, - method: {}, - query: {} - }); - - try { - await handler.getTokenFromRequestBody(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: content must be application/x-www-form-urlencoded'); - } - }); - - it('should return the bearer token', function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ - body: { access_token: 'foo' }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: {}, - query: {} - }); - - handler.getTokenFromRequestBody(request).should.equal('foo'); - }); - }); - - describe('getAccessToken()', function() { - it('should throw an error if `accessToken` is missing', function() { - const model = Model.from({ - getAccessToken: function() {} - }); - const handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: access token is invalid'); - }); - }); - - it('should throw an error if `accessToken.user` is missing', function() { - const model = Model.from({ - getAccessToken: function() { - return {}; - } - }); - const handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `getAccessToken()` did not return a `user` object'); - }); - }); - - it('should return an access token', function() { - const accessToken = { user: {} }; - const model = Model.from({ - getAccessToken: function() { - return accessToken; - } - }); - const handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(function(data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - const model = Model.from({ - getAccessToken: async function() { - return { user: {} }; - } - }); - const handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken('foo').should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const model = Model.from({ - getAccessToken: function() { - return { user: {} }; - } - }); - const handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken('foo').should.be.an.instanceOf(Promise); - }); - }); - - describe('validateAccessToken()', function() { - it('should throw an error if `accessToken` is expired', async function() { - const accessToken = { accessTokenExpiresAt: new Date(new Date() / 2) }; - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - try { - await handler.validateAccessToken(accessToken); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal('Invalid token: access token has expired'); - } - }); - - it('should return an access token', function() { - const accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - - handler.validateAccessToken(accessToken).should.equal(accessToken); - }); - }); - - describe('verifyScope()', function() { - it('should throw an error if `scope` is insufficient (deprecated)', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() { - return false; - } - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - return handler.verifyScope(['foo']) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InsufficientScopeError); - e.message.should.equal('Insufficient scope: authorized scope is insufficient'); - }); - }); - - it('should throw an error if `scope` is insufficient', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() { - return false; - } - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); - - return handler.verifyScope(['foo']) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InsufficientScopeError); - e.message.should.equal('Insufficient scope: authorized scope is insufficient'); - }); - }); - - it('should support promises (deprecated)', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() { - return true; - } - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); - }); - - it('should support promises', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() { - return true; - } - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); - - handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises (deprecated)', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() { - return true; - } - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'foo' }); - - handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() { - return true; - } - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: ['foo'] }); - - handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); - }); - }); - - describe('updateResponse()', function() { - it('should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() {} - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ['foo', 'biz'] }); - - response.headers.should.not.have.property('x-accepted-oauth-scopes'); - }); - - it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified (deprecated)', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() {} - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, scope: 'foo bar' }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ['foo', 'biz'] }); - - response.get('X-Accepted-OAuth-Scopes').should.equal('foo bar'); - }); - - it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() {} - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, scope: ['foo', 'bar'] }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ['foo', 'biz'] }); - - response.get('X-Accepted-OAuth-Scopes').should.equal('foo bar'); - }); - - it('should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() {} - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ['foo', 'biz'] }); - - response.headers.should.not.have.property('x-oauth-scopes'); - }); - - it('should set the `X-Authorized-OAuth-Scopes` header (deprecated)', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() {} - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, scope: 'foo bar' }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ['foo', 'biz'] }); - - response.get('X-OAuth-Scopes').should.equal('foo biz'); - }); - - it('should set the `X-Authorized-OAuth-Scopes` header', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: function() {} - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, scope: ['foo', 'bar'] }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ['foo', 'biz'] }); - - response.get('X-OAuth-Scopes').should.equal('foo biz'); - }); - }); +describe("AuthenticateHandler integration", function () { + describe("constructor()", function () { + it("should throw an error if `options.model` is missing", function () { + try { + new AuthenticateHandler(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `model`"); + } + }); + + it("should throw an error if the model does not implement `getAccessToken()`", function () { + try { + new AuthenticateHandler({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `getAccessToken()`", + ); + } + }); + + it("should throw an error if `scope` was given and `addAcceptedScopesHeader()` is missing", function () { + try { + new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + scope: ["foobar"], + }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `addAcceptedScopesHeader`"); + } + }); + + it("should throw an error if `scope` was given and `addAuthorizedScopesHeader()` is missing", function () { + try { + new AuthenticateHandler({ + addAcceptedScopesHeader: true, + model: { getAccessToken: function () {} }, + scope: ["foobar"], + }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Missing parameter: `addAuthorizedScopesHeader`", + ); + } + }); + + it("should throw an error if `scope` was given and the model does not implement `verifyScope()`", function () { + try { + new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: { getAccessToken: function () {} }, + scope: ["foobar"], + }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `verifyScope()`", + ); + } + }); + + it("should set the `model`", function () { + const model = Model.from({ getAccessToken: function () {} }); + const grantType = new AuthenticateHandler({ model: model }); + + grantType.model.should.equal(model); + }); + + it("should set the `scope`", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const grantType = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: "foobar", + }); + + grantType.scope.should.eql(["foobar"]); + }); + }); + + describe("handle()", function () { + it("should throw an error if `request` is missing or not a Request instance", async function () { + class Request {} // intentionally fake + const values = [undefined, null, {}, [], new Date(), new Request()]; + for (const request of values) { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + + try { + await handler.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: `request` must be an instance of Request", + ); + } + } + }); + + it("should throw an error if `response` is missing or not a Response instance", async function () { + class Response {} // intentionally fake + const values = [undefined, null, {}, [], new Date(), new Response()]; + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + + for (const response of values) { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + try { + await handler.handle(request, response); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: `response` must be an instance of Response", + ); + } + } + }); + + it("should set the `WWW-Authenticate` header if an unauthorized request error is thrown", async function () { + const model = Model.from({ + getAccessToken: function () { + throw new UnauthorizedRequestError(); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + response.get("WWW-Authenticate").should.equal('Bearer realm="Service"'); + } + }); + + it("should set the `WWW-Authenticate` header if an InvalidRequestError is thrown", function () { + const model = Model.from({ + getAccessToken: function () { + throw new InvalidRequestError(); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function () { + response + .get("WWW-Authenticate") + .should.equal('Bearer realm="Service",error="invalid_request"'); + }); + }); + + it("should set the `WWW-Authenticate` header if an InvalidTokenError is thrown", function () { + const model = Model.from({ + getAccessToken: function () { + throw new InvalidTokenError(); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function () { + response + .get("WWW-Authenticate") + .should.equal('Bearer realm="Service",error="invalid_token"'); + }); + }); + + it("should set the `WWW-Authenticate` header if an InsufficientScopeError is thrown", function () { + const model = Model.from({ + getAccessToken: function () { + throw new InsufficientScopeError(); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function () { + response + .get("WWW-Authenticate") + .should.equal('Bearer realm="Service",error="insufficient_scope"'); + }); + }); + + it("should throw the error if an oauth error is thrown", function () { + const model = Model.from({ + getAccessToken: function () { + throw new AccessDeniedError("Cannot request this access token"); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal("Cannot request this access token"); + }); + }); + + it("should throw a server error if a non-oauth error is thrown", function () { + const model = Model.from({ + getAccessToken: function () { + throw new Error("Unhandled exception"); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal("Unhandled exception"); + }); + }); + + it("should return an access token", function () { + const accessToken = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const model = Model.from({ + getAccessToken: function () { + return accessToken; + }, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: ["foo"], + }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function (data) { + data.should.equal(accessToken); + }) + .catch(should.fail); + }); + + it("should return an access token (deprecated)", function () { + const accessToken = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const model = Model.from({ + getAccessToken: function () { + return accessToken; + }, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: "foo", + }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function (data) { + data.should.equal(accessToken); + }) + .catch(should.fail); + }); + }); + + describe("getTokenFromRequest()", function () { + it("should throw an error if more than one authentication method is used", async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: { access_token: "foo" }, + }); + + try { + await handler.getTokenFromRequest(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: only one authentication method is allowed", + ); + } + }); + + it("should throw an error if `accessToken` is missing", async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getTokenFromRequest(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnauthorizedRequestError); + e.message.should.equal("Unauthorized request: no authentication given"); + } + }); + }); + + describe("getTokenFromRequestHeader()", function () { + it("should throw an error if the token is malformed", async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: "foobar", + }, + method: {}, + query: {}, + }); + + try { + await handler.getTokenFromRequestHeader(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: malformed authorization header", + ); + } + }); + + it("should return the bearer token", function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: {}, + }); + + const bearerToken = handler.getTokenFromRequestHeader(request); + + bearerToken.should.equal("foo"); + }); + }); + + describe("getTokenFromRequestQuery()", function () { + it("should throw an error if the query contains a token", async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + + try { + await handler.getTokenFromRequestQuery(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: do not send bearer tokens in query URLs", + ); + } + }); + + it("should return the bearer token if `allowBearerTokensInQueryString` is true", function () { + const handler = new AuthenticateHandler({ + allowBearerTokensInQueryString: true, + model: { getAccessToken: function () {} }, + }); + + handler + .getTokenFromRequestQuery({ query: { access_token: "foo" } }) + .should.equal("foo"); + }); + }); + + describe("getTokenFromRequestBody()", function () { + it("should throw an error if the method is `GET`", async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: { access_token: "foo" }, + headers: {}, + method: "GET", + query: {}, + }); + + try { + await handler.getTokenFromRequestBody(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: token may not be passed in the body when using the GET verb", + ); + } + }); + + it("should throw an error if the media type is not `application/x-www-form-urlencoded`", async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: { access_token: "foo" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getTokenFromRequestBody(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: content must be application/x-www-form-urlencoded", + ); + } + }); + + it("should return the bearer token", function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: { access_token: "foo" }, + headers: { + "content-type": "application/x-www-form-urlencoded", + "transfer-encoding": "chunked", + }, + method: {}, + query: {}, + }); + + handler.getTokenFromRequestBody(request).should.equal("foo"); + }); + }); + + describe("getAccessToken()", function () { + it("should throw an error if `accessToken` is missing", function () { + const model = Model.from({ + getAccessToken: function () {}, + }); + const handler = new AuthenticateHandler({ model: model }); + + return handler + .getAccessToken("foo") + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal("Invalid token: access token is invalid"); + }); + }); + + it("should throw an error if `accessToken.user` is missing", function () { + const model = Model.from({ + getAccessToken: function () { + return {}; + }, + }); + const handler = new AuthenticateHandler({ model: model }); + + return handler + .getAccessToken("foo") + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + "Server error: `getAccessToken()` did not return a `user` object", + ); + }); + }); + + it("should return an access token", function () { + const accessToken = { user: {} }; + const model = Model.from({ + getAccessToken: function () { + return accessToken; + }, + }); + const handler = new AuthenticateHandler({ model: model }); + + return handler + .getAccessToken("foo") + .then(function (data) { + data.should.equal(accessToken); + }) + .catch(should.fail); + }); + + it("should support promises", function () { + const model = Model.from({ + getAccessToken: async function () { + return { user: {} }; + }, + }); + const handler = new AuthenticateHandler({ model: model }); + + handler.getAccessToken("foo").should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const model = Model.from({ + getAccessToken: function () { + return { user: {} }; + }, + }); + const handler = new AuthenticateHandler({ model: model }); + + handler.getAccessToken("foo").should.be.an.instanceOf(Promise); + }); + }); + + describe("validateAccessToken()", function () { + it("should throw an error if `accessToken` is expired", async function () { + const accessToken = { accessTokenExpiresAt: new Date(new Date() / 2) }; + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + + try { + await handler.validateAccessToken(accessToken); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal("Invalid token: access token has expired"); + } + }); + + it("should return an access token", function () { + const accessToken = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + + handler.validateAccessToken(accessToken).should.equal(accessToken); + }); + }); + + describe("verifyScope()", function () { + it("should throw an error if `scope` is insufficient (deprecated)", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return false; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: "foo", + }); + + return handler + .verifyScope(["foo"]) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InsufficientScopeError); + e.message.should.equal( + "Insufficient scope: authorized scope is insufficient", + ); + }); + }); + + it("should throw an error if `scope` is insufficient", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return false; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: ["foo"], + }); + + return handler + .verifyScope(["foo"]) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InsufficientScopeError); + e.message.should.equal( + "Insufficient scope: authorized scope is insufficient", + ); + }); + }); + + it("should support promises (deprecated)", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: "foo", + }); + + handler.verifyScope(["foo"]).should.be.an.instanceOf(Promise); + }); + + it("should support promises", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: ["foo"], + }); + + handler.verifyScope(["foo"]).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises (deprecated)", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: "foo", + }); + + handler.verifyScope(["foo"]).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: ["foo"], + }); + + handler.verifyScope(["foo"]).should.be.an.instanceOf(Promise); + }); + }); + + describe("updateResponse()", function () { + it("should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model: model, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ["foo", "biz"] }); + + response.headers.should.not.have.property("x-accepted-oauth-scopes"); + }); + + it("should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified (deprecated)", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model: model, + scope: "foo bar", + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ["foo", "biz"] }); + + response.get("X-Accepted-OAuth-Scopes").should.equal("foo bar"); + }); + + it("should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model: model, + scope: ["foo", "bar"], + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ["foo", "biz"] }); + + response.get("X-Accepted-OAuth-Scopes").should.equal("foo bar"); + }); + + it("should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model: model, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ["foo", "biz"] }); + + response.headers.should.not.have.property("x-oauth-scopes"); + }); + + it("should set the `X-Authorized-OAuth-Scopes` header (deprecated)", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model: model, + scope: "foo bar", + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ["foo", "biz"] }); + + response.get("X-OAuth-Scopes").should.equal("foo biz"); + }); + + it("should set the `X-Authorized-OAuth-Scopes` header", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model: model, + scope: ["foo", "bar"], + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ["foo", "biz"] }); + + response.get("X-OAuth-Scopes").should.equal("foo biz"); + }); + }); }); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index d9004cd3..dcabe42a 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -1,1524 +1,2015 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AccessDeniedError = require('../../../lib/errors/access-denied-error'); -const AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); -const AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); -const CodeResponseType = require('../../../lib/response-types/code-response-type'); -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const InvalidClientError = require('../../../lib/errors/invalid-client-error'); -const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -const InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); -const UnsupportedResponseTypeError = require('../../../lib/errors/unsupported-response-type-error'); -const Model = require('../../../lib/model'); -const Request = require('../../../lib/request'); -const Response = require('../../../lib/response'); -const ServerError = require('../../../lib/errors/server-error'); -const UnauthorizedClientError = require('../../../lib/errors/unauthorized-client-error'); -const should = require('chai').should(); -const url = require('url'); +const AccessDeniedError = require("../../../lib/errors/access-denied-error"); +const AuthenticateHandler = require("../../../lib/handlers/authenticate-handler"); +const AuthorizeHandler = require("../../../lib/handlers/authorize-handler"); +const CodeResponseType = require("../../../lib/response-types/code-response-type"); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const InvalidClientError = require("../../../lib/errors/invalid-client-error"); +const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); +const InvalidScopeError = require("../../../lib/errors/invalid-scope-error"); +const UnsupportedResponseTypeError = require("../../../lib/errors/unsupported-response-type-error"); +const Model = require("../../../lib/model"); +const Request = require("../../../lib/request"); +const Response = require("../../../lib/response"); +const ServerError = require("../../../lib/errors/server-error"); +const UnauthorizedClientError = require("../../../lib/errors/unauthorized-client-error"); +const should = require("chai").should(); +const url = require("url"); const createModel = (model = {}) => { - return Model.from({ - getAccessToken: () => should.fail(), - getClient: () => should.fail(), - saveAuthorizationCode: () => should.fail(), - ...model - }); + return Model.from({ + getAccessToken: () => should.fail(), + getClient: () => should.fail(), + saveAuthorizationCode: () => should.fail(), + ...model, + }); }; /** * Test `AuthorizeHandler` integration. */ -describe('AuthorizeHandler integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.authorizationCodeLifetime` is missing', function() { - try { - new AuthorizeHandler(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `authorizationCodeLifetime`'); - } - }); - - it('should throw an error if `options.model` is missing', function() { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120 }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if the model does not implement `getClient()`', function() { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getClient()`'); - } - }); - - it('should throw an error if the model does not implement `saveAuthorizationCode()`', function() { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: { getClient: () => should.fail() } }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `saveAuthorizationCode()`'); - } - }); - - it('should throw an error if the model does not implement `getAccessToken()`', function() { - const model = Model.from({ - getClient: () => should.fail(), - saveAuthorizationCode: () => should.fail() - }); - - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); - } - }); - - it('should set the `authorizationCodeLifetime`', function() { - const model = createModel(); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - handler.authorizationCodeLifetime.should.equal(120); - }); - - it('should throw if the custom `authenticateHandler` does not implement a `handle` method', function () { - const model = createModel(); - const authenticateHandler = {}; // misses handle() method - - try { - new AuthorizeHandler({ authenticateHandler, authorizationCodeLifetime: 120, model }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: authenticateHandler does not implement `handle()`'); - } - }); - - it('should set the default `authenticateHandler`, if no custom one is passed', function() { - const model = createModel(); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); - }); - - it('should set the custom `authenticateHandler`, if valid', function () { - const model = createModel(); - - class CustomAuthenticateHandler { - async handle () {} - } - - const authenticateHandler = new CustomAuthenticateHandler(); - const handler = new AuthorizeHandler({ authenticateHandler, authorizationCodeLifetime: 120, model }); - handler.authenticateHandler.should.be.an.instanceOf(CustomAuthenticateHandler); - handler.authenticateHandler.should.not.be.an.instanceOf(AuthenticateHandler); - }); - - it('should set the `model`', function() { - const model = createModel(); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - handler.model.should.equal(model); - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', async function() { - const model = createModel(); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - try { - await handler.handle(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); - } - }); - - it('should throw an error if `response` is missing', async function() { - const model = createModel(); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await handler.handle(request); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `response` must be an instance of Response'); - } - }); - - it('should redirect to an error response if user denied access', async function() { - const client = { - id: 'client-12345', - grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'] - }; - const model = createModel({ - getAccessToken: async function(_token) { - _token.should.equal('foobarbazmootoken'); - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function(clientId, clientSecret) { - clientId.should.equal(client.id); - (clientSecret === null).should.equal(true); - return { ...client }; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: client.id, - response_type: 'code' - }, - method: {}, - headers: { - 'Authorization': 'Bearer foobarbazmootoken' - }, - query: { - state: 'foobar', - allowed: 'false' - } - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal('Access denied: user denied access to application'); - response - .get('location') - .should - .equal('http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar'); - } - }); - - it('should redirect to an error response if a non-oauth error is thrown', async function() { - const model = createModel({ - getAccessToken: async function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function() { - return { - grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'] - }; - }, - saveAuthorizationCode: async function() { - throw new CustomError('Unhandled exception'); - } - }); - class CustomError extends Error {} - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); // non-oauth-errors are converted to ServerError - e.message.should.equal('Unhandled exception'); - response - .get('location') - .should - .equal('http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar'); - } - }); - - it('should redirect to an error response if an oauth error is thrown', async function() { - const model = createModel({ - getAccessToken: async function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: async function() { - throw new AccessDeniedError('Cannot request this auth code'); - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal('Cannot request this auth code'); - response - .get('location') - .should - .equal('http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar'); - } - }); - - it('should redirect to a successful response with `code` and `state` if successful', async function() { - const client = { - id: 'client-12343434', - grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'] - }; - const model = createModel({ - getAccessToken: async function(_token) { - _token.should.equal('foobarbaztokenmoo'); - return { - client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function(clientId, clientSecret) { - clientId.should.equal(client.id); - (clientSecret === null).should.equal(true); - return { ...client }; - }, - saveAuthorizationCode: async function() { - return { - authorizationCode: 'fooobar-long-authzcode-?', - client - }; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: client.id, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foobarbaztokenmoo' - }, - method: {}, - query: { - state: 'foobarbazstatemoo' - } - }); - const response = new Response({ body: {}, headers: {} }); - const data = await handler.handle(request, response); - data.authorizationCode.should.equal('fooobar-long-authzcode-?'); - data.client.should.deep.equal(client); - response.status.should.equal(302); - response - .get('location') - .should - .equal('http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo'); - }); - - it('should redirect to an error response if `scope` is invalid', async function() { - const model = createModel({ - getAccessToken: async function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: async function() { - return {}; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - scope: [], - state: 'foobar' - } - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal('Invalid parameter: `scope`'); - response.status.should.equal(302); - response.get('location').should.equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar'); - } - }); - - it('should redirect to a successful response if `model.validateScope` is not defined', async function() { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = Model.from({ - getAccessToken: function() { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return client; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 'fooobar-long-authzcode-?', client }; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - scope: 'read', - state: 'foobarbazstatemoo' - } - }); - const response = new Response({ body: {}, headers: {} }); - const data = await handler.handle(request, response); - data.should.deep.equal({ - authorizationCode: 'fooobar-long-authzcode-?', - client: client - }); - response.status.should.equal(302); - response - .get('location') - .should - .equal('http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo'); - }); - - it('should redirect to an error response if `scope` is insufficient (validateScope)', async function() { - const client = { id: 12345, grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = Model.from({ - getAccessToken: async function() { - return { - client: client, - user: { name: 'foouser' }, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function() { - return client; - }, - saveAuthorizationCode: async function() { - return { authorizationCode: 12345, client }; - }, - validateScope: async function(_user, _client, _scope) { - _scope.should.eql(['read']); - return false; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - scope: 'read', - state: 'foobar' - } - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch(e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal('Invalid scope: Requested scope is invalid'); - response.status.should.equal(302); - response - .get('location') - .should - .equal('http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar'); - } - }); - - it('should redirect to an error response if `state` is missing', async function() { - const model = createModel({ - getAccessToken: async function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: async function() { - throw new AccessDeniedError('Cannot request this auth code'); - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: {} - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `state`'); - response.status.should.equal(302); - response - .get('location') - .should - .equal('http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60'); - } - }); - - it('should redirect to an error response if `response_type` is invalid', async function() { - const model = Model.from({ - getAccessToken: async function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: () => should.fail() // should fail before call - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: 12345, - response_type: 'test' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedResponseTypeError); - e.message.should.equal('Unsupported response type: `response_type` is not supported'); - response.status.should.equal(302); - response - .get('location') - .should - .equal('http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar'); - } - }); - - it('should return the `code` if successful', async function() { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = Model.from({ - getAccessToken: async function() { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function() { - return client; - }, - generateAuthorizationCode: async () => 'some-code', - saveAuthorizationCode: async function(code) { - return { authorizationCode: code.authorizationCode, client: client }; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - const response = new Response({ body: {}, headers: {} }); - - const data = await handler.handle(request, response); - data.should.eql({ - authorizationCode: 'some-code', - client: client - }); - }); - - it('should return the `code` if successful (full model implementation)', async function () { - const user = { name: 'fooUser' }; - const state = 'fooobarstatebaz'; - const scope = ['read']; - const client = { - id: 'client-1322132131', - grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'] - }; - const authorizationCode = 'long-authz-code'; - const accessTokenDoc = { - accessToken: 'some-access-token-code', - client, - user, - scope, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - const model = Model.from({ - getClient: async function (clientId, clientSecret) { - clientId.should.equal(client.id); - (clientSecret === null).should.equal(true); - return { ...client }; - }, - getAccessToken: async function (_token) { - _token.should.equal(accessTokenDoc.accessToken); - return { ...accessTokenDoc }; - }, - verifyScope: async function (_tokenDoc, _scope) { - _tokenDoc.should.equal(accessTokenDoc); - _scope.should.eql(accessTokenDoc.scope); - return true; - }, - validateScope: async function (_user, _client, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return _scope; - }, - generateAuthorizationCode: async function (_client, _user, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return authorizationCode; - }, - saveAuthorizationCode: async function (code, _client, _user) { - code.authorizationCode.should.equal(authorizationCode); - code.expiresAt.should.be.instanceOf(Date); - _user.should.deep.equal(user); - _client.should.deep.equal(client); - return { ...code, client, user }; - } - }); - - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { - client_id: client.id, - response_type: 'code' - }, - headers: { - 'Authorization': `Bearer ${accessTokenDoc.accessToken}` - }, - method: {}, - query: { state, scope: scope.join(' ') } - }); - - const response = new Response({ body: {}, headers: {} }); - const data = await handler.handle(request, response); - data.scope.should.eql(scope); - data.client.should.deep.equal(client); - data.user.should.deep.equal(user); - data.expiresAt.should.be.instanceOf(Date); - data.redirectUri.should.equal(client.redirectUris[0]); - response.status.should.equal(302); - response - .get('location') - .should - .equal('http://example.com/cb?code=long-authz-code&state=fooobarstatebaz'); - }); - - it('should support a custom `authenticateHandler`', async function () { - const user = { name: 'user1' }; - const authenticateHandler = { - handle: async function () { - // all good - return { ...user }; - } - }; - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = Model.from({ - getAccessToken: async function() { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: async function() { - return client; - }, - generateAuthorizationCode: async () => 'some-code', - saveAuthorizationCode: async function(code) { - return { authorizationCode: code.authorizationCode, client: client }; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model, authenticateHandler }); - const request = new Request({ - body: { - client_id: 12345, - response_type: 'code' - }, - headers: { - 'Authorization': 'Bearer foo' - }, - method: {}, - query: { - state: 'foobar' - } - }); - const response = new Response({ body: {}, headers: {} }); - - const data = await handler.handle(request, response); - data.should.eql({ - authorizationCode: 'some-code', - client: client - }); - }); - }); - - describe('generateAuthorizationCode()', function() { - it('should return an auth code', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - return handler.generateAuthorizationCode() - .then(function(data) { - data.should.be.a.sha256(); - }) - .catch(should.fail); - }); - - it('should support promises', function() { - const model = Model.from({ - generateAuthorizationCode: async function() { - return {}; - }, - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const model = Model.from({ - generateAuthorizationCode: function() { - return {}; - }, - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); - }); - }); - - describe('getAuthorizationCodeLifetime()', function() { - it('should return a date', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); - }); - }); - - describe('validateRedirectUri()', function() { - it('should support empty method', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - handler.validateRedirectUri('http://example.com/a', { redirectUris: ['http://example.com/a'] }).should.be.an.instanceOf(Promise); - }); - - it('should support promises', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {}, - validateRedirectUri: async function() { - return true; - } - }); - - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - handler.validateRedirectUri('http://example.com/a', { }).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {}, - validateRedirectUri: function() { - return true; - } - }); - - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - handler.validateRedirectUri('http://example.com/a', { }).should.be.an.instanceOf(Promise); - }); - }); - - describe('getClient()', function() { - it('should throw an error if `client_id` is missing', async function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `client_id`'); - } - }); - - it('should throw an error if `client_id` is invalid', async function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { client_id: 'øå€£‰', response_type: 'code' }, headers: {}, method: {}, query: {} }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); - } - }); - - it('should throw an error if `client.redirectUri` is invalid', async function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); - } - }); - - it('should throw an error if `client` is missing', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: client credentials are invalid'); - }); - }); - - it('should throw an error if `client.grants` is missing', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() { - return {}; - }, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: missing client `grants`'); - }); - }); - - it('should throw an error if `client` is unauthorized', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() { - return { grants: [] }; - }, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal('Unauthorized client: `grant_type` is invalid'); - }); - }); - - it('should throw an error if `client.redirectUri` is missing', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() { return { grants: ['authorization_code'] }; }, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: missing client `redirectUri`'); - }); - }); - - it('should throw an error if `client.redirectUri` is not equal to `redirectUri`', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['https://example.com'] }; - }, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { client_id: 12345, response_type: 'code', redirect_uri: 'https://foobar.com' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: `redirect_uri` does not match client value'); - }); - }); - - it('should support promises', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: async function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {} - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {} - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - describe('with `client_id` in the request query', function() { - it('should return a client', function() { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() { - return client; - }, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: { client_id: 12345 } }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - }); - - describe('getScope()', function() { - it('should throw an error if `scope` is invalid', async function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { scope: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - await handler.getScope(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal('Invalid parameter: `scope`'); - } - }); - - describe('with `scope` in the request body', function() { - it('should return the scope', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, query: {} }); - - handler.getScope(request).should.eql(['foo']); - }); - }); - - describe('with `scope` in the request query', function() { - it('should return the scope', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: { scope: 'foo' } }); - - handler.getScope(request).should.eql(['foo']); - }); - }); - }); - - describe('getState()', function() { - it('should throw an error if `allowEmptyState` is false and `state` is missing', async function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await handler.getState(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `state`'); - } - }); - - it('should allow missing `state` if `allowEmptyState` is valid', function () { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ allowEmptyState: true, authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - const state = handler.getState(request); - should.equal(state, undefined); - }); - - it('should throw an error if `state` is invalid', async function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'øå€£‰' } }); - - try { - await handler.getState(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `state`'); - } - }); - - describe('with `state` in the request body', function() { - it('should return the state', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, query: {} }); - - handler.getState(request).should.equal('foobar'); - }); - }); - - describe('with `state` in the request query', function() { - it('should return the state', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: { state: 'foobar' } }); - - handler.getState(request).should.equal('foobar'); - }); - }); - }); - - describe('getUser()', function() { - it('should throw an error if `user` is missing', function() { - const authenticateHandler = { handle: function() {} }; - const model = Model.from({ - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - const response = new Response(); - - return handler.getUser(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `handle()` did not return a `user` object'); - }); - }); - - it('should return a user', function() { - const user = {}; - const model = Model.from({ - getAccessToken: function() { - return { - user: user, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - const response = new Response({ body: {}, headers: {} }); - - return handler.getUser(request, response) - .then(function(data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - }); - - describe('saveAuthorizationCode()', function() { - it('should return an auth code', function() { - const authorizationCode = {}; - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() { - return authorizationCode; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - return handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz') - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); - - it('should support promises when calling `model.saveAuthorizationCode()`', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: async function() { - return {}; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); - }); - - it('should support non-promises when calling `model.saveAuthorizationCode()`', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() { - return {}; - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); - }); - }); - - describe('getResponseType()', function() { - it('should throw an error if `response_type` is missing', async function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await handler.getResponseType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `response_type`'); - } - }); - - it('should throw an error if `response_type` is not `code`', async function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { response_type: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - await handler.getResponseType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedResponseTypeError); - e.message.should.equal('Unsupported response type: `response_type` is not supported'); - } - }); - - describe('with `response_type` in the request body', function() { - it('should return a response type', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, query: {} }); - const ResponseType = handler.getResponseType(request); - - ResponseType.should.equal(CodeResponseType); - }); - }); - - describe('with `response_type` in the request query', function() { - it('should return a response type', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: { response_type: 'code' } }); - const ResponseType = handler.getResponseType(request); - - ResponseType.should.equal(CodeResponseType); - }); - }); - }); - - describe('buildSuccessRedirectUri()', function() { - it('should return a redirect uri', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const responseType = new CodeResponseType(12345); - const redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); - - url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); - }); - }); - - describe('buildErrorRedirectUri()', function() { - it('should set `error_description` if available', function() { - const error = new InvalidClientError('foo bar'); - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); - - url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=foo%20bar'); - }); - - it('should return a redirect uri', function() { - const error = new InvalidClientError(); - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); - - url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=Bad%20Request'); - }); - }); - - describe('updateResponse()', function() { - it('should set the `location` header', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const response = new Response({ body: {}, headers: {} }); - const uri = url.parse('http://example.com/cb'); - - handler.updateResponse(response, uri, 'foobar'); - - response.get('location').should.equal('http://example.com/cb?state=foobar'); - }); - }); - - describe('getCodeChallengeMethod()', function() { - it('should throw if the code challenge method is not supported', async function () { - const methods = ['plain', 'foo', ' ', '0', true, {}, []]; - - for (const method of methods) { - const model = Model.from({ - getAccessToken: function () { - }, - getClient: function () { - }, - saveAuthorizationCode: function () { - } - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: { code_challenge_method: method }, headers: {}, method: {}, query: {} }); - - try { - await handler.getCodeChallengeMethod(request); - should.fail(); - } catch (e) { - if (method === 'plain') { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); - } - else { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal(`Invalid request: transform algorithm '${method}' not supported`); - } - } - } - }); - - it('should get code challenge method', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {code_challenge_method: 'S256'}, headers: {}, method: {}, query: {} }); - - const codeChallengeMethod = handler.getCodeChallengeMethod(request); - codeChallengeMethod.should.equal('S256'); - }); - - it('should throw if the code challenge method is not supported', async function () { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {code_challenge_method: 'foo'}, headers: {}, method: {}, query: {} }); - - try { - await handler.getCodeChallengeMethod(request); - - should.fail(); - } catch (e) { - // defined in RFC 7636 - 4.4 - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: transform algorithm \'foo\' not supported'); - } - }); - - it('should get default code challenge method S256 if missing (default)', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - const codeChallengeMethod = handler.getCodeChallengeMethod(request); - codeChallengeMethod.should.equal('S256'); - }); - - it('should get default code challenge method plain if missing and plain PKCE is enabled', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, enablePlainPKCE: true, model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - const codeChallengeMethod = handler.getCodeChallengeMethod(request); - codeChallengeMethod.should.equal('plain'); - }); - }); - - describe('getCodeChallenge()', function() { - it('should get code challenge', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - const request = new Request({ body: {code_challenge: 'challenge'}, headers: {}, method: {}, query: {} }); - - const codeChallengeMethod = handler.getCodeChallenge(request); - codeChallengeMethod.should.equal('challenge'); - }); - }); +describe("AuthorizeHandler integration", function () { + describe("constructor()", function () { + it("should throw an error if `options.authorizationCodeLifetime` is missing", function () { + try { + new AuthorizeHandler(); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Missing parameter: `authorizationCodeLifetime`", + ); + } + }); + + it("should throw an error if `options.model` is missing", function () { + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120 }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `model`"); + } + }); + + it("should throw an error if the model does not implement `getClient()`", function () { + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `getClient()`", + ); + } + }); + + it("should throw an error if the model does not implement `saveAuthorizationCode()`", function () { + try { + new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: { getClient: () => should.fail() }, + }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `saveAuthorizationCode()`", + ); + } + }); + + it("should throw an error if the model does not implement `getAccessToken()`", function () { + const model = Model.from({ + getClient: () => should.fail(), + saveAuthorizationCode: () => should.fail(), + }); + + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `getAccessToken()`", + ); + } + }); + + it("should set the `authorizationCodeLifetime`", function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.authorizationCodeLifetime.should.equal(120); + }); + + it("should throw if the custom `authenticateHandler` does not implement a `handle` method", function () { + const model = createModel(); + const authenticateHandler = {}; // misses handle() method + + try { + new AuthorizeHandler({ + authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: authenticateHandler does not implement `handle()`", + ); + } + }); + + it("should set the default `authenticateHandler`, if no custom one is passed", function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); + }); + + it("should set the custom `authenticateHandler`, if valid", function () { + const model = createModel(); + + class CustomAuthenticateHandler { + async handle() {} + } + + const authenticateHandler = new CustomAuthenticateHandler(); + const handler = new AuthorizeHandler({ + authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + handler.authenticateHandler.should.be.an.instanceOf( + CustomAuthenticateHandler, + ); + handler.authenticateHandler.should.not.be.an.instanceOf( + AuthenticateHandler, + ); + }); + + it("should set the `model`", function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + handler.model.should.equal(model); + }); + }); + + describe("handle()", function () { + it("should throw an error if `request` is missing", async function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + try { + await handler.handle(); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: `request` must be an instance of Request", + ); + } + }); + + it("should throw an error if `response` is missing", async function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handle(request); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: `response` must be an instance of Response", + ); + } + }); + + it("should redirect to an error response if user denied access", async function () { + const client = { + id: "client-12345", + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const model = createModel({ + getAccessToken: async function (_token) { + _token.should.equal("foobarbazmootoken"); + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function (clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: client.id, + response_type: "code", + }, + method: {}, + headers: { + Authorization: "Bearer foobarbazmootoken", + }, + query: { + state: "foobar", + allowed: "false", + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal( + "Access denied: user denied access to application", + ); + response + .get("location") + .should.equal( + "http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar", + ); + } + }); + + it("should redirect to an error response if a non-oauth error is thrown", async function () { + const model = createModel({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + }, + saveAuthorizationCode: async function () { + throw new CustomError("Unhandled exception"); + }, + }); + class CustomError extends Error {} + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + }, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: { + state: "foobar", + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); // non-oauth-errors are converted to ServerError + e.message.should.equal("Unhandled exception"); + response + .get("location") + .should.equal( + "http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar", + ); + } + }); + + it("should redirect to an error response if an oauth error is thrown", async function () { + const model = createModel({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + }, + saveAuthorizationCode: async function () { + throw new AccessDeniedError("Cannot request this auth code"); + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + }, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: { + state: "foobar", + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal("Cannot request this auth code"); + response + .get("location") + .should.equal( + "http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar", + ); + } + }); + + it("should redirect to a successful response with `code` and `state` if successful", async function () { + const client = { + id: "client-12343434", + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const model = createModel({ + getAccessToken: async function (_token) { + _token.should.equal("foobarbaztokenmoo"); + return { + client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function (clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + }, + saveAuthorizationCode: async function () { + return { + authorizationCode: "fooobar-long-authzcode-?", + client, + }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: client.id, + response_type: "code", + }, + headers: { + Authorization: "Bearer foobarbaztokenmoo", + }, + method: {}, + query: { + state: "foobarbazstatemoo", + }, + }); + const response = new Response({ body: {}, headers: {} }); + const data = await handler.handle(request, response); + data.authorizationCode.should.equal("fooobar-long-authzcode-?"); + data.client.should.deep.equal(client); + response.status.should.equal(302); + response + .get("location") + .should.equal( + "http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo", + ); + }); + + it("should redirect to an error response if `scope` is invalid", async function () { + const model = createModel({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + }, + saveAuthorizationCode: async function () { + return {}; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + }, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: { + scope: [], + state: "foobar", + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal("Invalid parameter: `scope`"); + response.status.should.equal(302); + response + .get("location") + .should.equal( + "http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar", + ); + } + }); + + it("should redirect to a successful response if `model.validateScope` is not defined", async function () { + const client = { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const model = Model.from({ + getAccessToken: function () { + return { + client: client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: function () { + return client; + }, + saveAuthorizationCode: function () { + return { authorizationCode: "fooobar-long-authzcode-?", client }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + }, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: { + scope: "read", + state: "foobarbazstatemoo", + }, + }); + const response = new Response({ body: {}, headers: {} }); + const data = await handler.handle(request, response); + data.should.deep.equal({ + authorizationCode: "fooobar-long-authzcode-?", + client: client, + }); + response.status.should.equal(302); + response + .get("location") + .should.equal( + "http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo", + ); + }); + + it("should redirect to an error response if `scope` is insufficient (validateScope)", async function () { + const client = { + id: 12345, + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const model = Model.from({ + getAccessToken: async function () { + return { + client: client, + user: { name: "foouser" }, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return client; + }, + saveAuthorizationCode: async function () { + return { authorizationCode: 12345, client }; + }, + validateScope: async function (_user, _client, _scope) { + _scope.should.eql(["read"]); + return false; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + }, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: { + scope: "read", + state: "foobar", + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal("Invalid scope: Requested scope is invalid"); + response.status.should.equal(302); + response + .get("location") + .should.equal( + "http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar", + ); + } + }); + + it("should redirect to an error response if `state` is missing", async function () { + const model = createModel({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + }, + saveAuthorizationCode: async function () { + throw new AccessDeniedError("Cannot request this auth code"); + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + }, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Missing parameter: `state`"); + response.status.should.equal(302); + response + .get("location") + .should.equal( + "http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60", + ); + } + }); + + it("should redirect to an error response if `response_type` is invalid", async function () { + const model = Model.from({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + }, + saveAuthorizationCode: () => should.fail(), // should fail before call + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "test", + }, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: { + state: "foobar", + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedResponseTypeError); + e.message.should.equal( + "Unsupported response type: `response_type` is not supported", + ); + response.status.should.equal(302); + response + .get("location") + .should.equal( + "http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar", + ); + } + }); + + it("should return the `code` if successful", async function () { + const client = { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const model = Model.from({ + getAccessToken: async function () { + return { + client: client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return client; + }, + generateAuthorizationCode: async () => "some-code", + saveAuthorizationCode: async function (code) { + return { authorizationCode: code.authorizationCode, client: client }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + }, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: { + state: "foobar", + }, + }); + const response = new Response({ body: {}, headers: {} }); + + const data = await handler.handle(request, response); + data.should.eql({ + authorizationCode: "some-code", + client: client, + }); + }); + + it("should return the `code` if successful (full model implementation)", async function () { + const user = { name: "fooUser" }; + const state = "fooobarstatebaz"; + const scope = ["read"]; + const client = { + id: "client-1322132131", + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const authorizationCode = "long-authz-code"; + const accessTokenDoc = { + accessToken: "some-access-token-code", + client, + user, + scope, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const model = Model.from({ + getClient: async function (clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + }, + getAccessToken: async function (_token) { + _token.should.equal(accessTokenDoc.accessToken); + return { ...accessTokenDoc }; + }, + verifyScope: async function (_tokenDoc, _scope) { + _tokenDoc.should.equal(accessTokenDoc); + _scope.should.eql(accessTokenDoc.scope); + return true; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return _scope; + }, + generateAuthorizationCode: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return authorizationCode; + }, + saveAuthorizationCode: async function (code, _client, _user) { + code.authorizationCode.should.equal(authorizationCode); + code.expiresAt.should.be.instanceOf(Date); + _user.should.deep.equal(user); + _client.should.deep.equal(client); + return { ...code, client, user }; + }, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: client.id, + response_type: "code", + }, + headers: { + Authorization: `Bearer ${accessTokenDoc.accessToken}`, + }, + method: {}, + query: { state, scope: scope.join(" ") }, + }); + + const response = new Response({ body: {}, headers: {} }); + const data = await handler.handle(request, response); + data.scope.should.eql(scope); + data.client.should.deep.equal(client); + data.user.should.deep.equal(user); + data.expiresAt.should.be.instanceOf(Date); + data.redirectUri.should.equal(client.redirectUris[0]); + response.status.should.equal(302); + response + .get("location") + .should.equal( + "http://example.com/cb?code=long-authz-code&state=fooobarstatebaz", + ); + }); + + it("should support a custom `authenticateHandler`", async function () { + const user = { name: "user1" }; + const authenticateHandler = { + handle: async function () { + // all good + return { ...user }; + }, + }; + const client = { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const model = Model.from({ + getAccessToken: async function () { + return { + client: client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return client; + }, + generateAuthorizationCode: async () => "some-code", + saveAuthorizationCode: async function (code) { + return { authorizationCode: code.authorizationCode, client: client }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + authenticateHandler, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + }, + headers: { + Authorization: "Bearer foo", + }, + method: {}, + query: { + state: "foobar", + }, + }); + const response = new Response({ body: {}, headers: {} }); + + const data = await handler.handle(request, response); + data.should.eql({ + authorizationCode: "some-code", + client: client, + }); + }); + }); + + describe("generateAuthorizationCode()", function () { + it("should return an auth code", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .generateAuthorizationCode() + .then(function (data) { + data.should.be.a.sha256(); + }) + .catch(should.fail); + }); + + it("should support promises", function () { + const model = Model.from({ + generateAuthorizationCode: async function () { + return {}; + }, + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const model = Model.from({ + generateAuthorizationCode: function () { + return {}; + }, + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); + }); + }); + + describe("getAuthorizationCodeLifetime()", function () { + it("should return a date", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); + }); + }); + + describe("validateRedirectUri()", function () { + it("should support empty method", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .validateRedirectUri("http://example.com/a", { + redirectUris: ["http://example.com/a"], + }) + .should.be.an.instanceOf(Promise); + }); + + it("should support promises", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + validateRedirectUri: async function () { + return true; + }, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .validateRedirectUri("http://example.com/a", {}) + .should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + validateRedirectUri: function () { + return true; + }, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .validateRedirectUri("http://example.com/a", {}) + .should.be.an.instanceOf(Promise); + }); + }); + + describe("getClient()", function () { + it("should throw an error if `client_id` is missing", async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: "code" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Missing parameter: `client_id`"); + } + }); + + it("should throw an error if `client_id` is invalid", async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: "øå€£‰", response_type: "code" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `client_id`"); + } + }); + + it("should throw an error if `client.redirectUri` is invalid", async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + redirect_uri: "foobar", + }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: `redirect_uri` is not a valid URI", + ); + } + }); + + it("should throw an error if `client` is missing", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: "code" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + "Invalid client: client credentials are invalid", + ); + }); + }); + + it("should throw an error if `client.grants` is missing", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return {}; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: "code" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal("Invalid client: missing client `grants`"); + }); + }); + + it("should throw an error if `client` is unauthorized", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return { grants: [] }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: "code" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(UnauthorizedClientError); + e.message.should.equal( + "Unauthorized client: `grant_type` is invalid", + ); + }); + }); + + it("should throw an error if `client.redirectUri` is missing", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return { grants: ["authorization_code"] }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: "code" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + "Invalid client: missing client `redirectUri`", + ); + }); + }); + + it("should throw an error if `client.redirectUri` is not equal to `redirectUri`", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return { + grants: ["authorization_code"], + redirectUris: ["https://example.com"], + }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: "code", + redirect_uri: "https://foobar.com", + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + "Invalid client: `redirect_uri` does not match client value", + ); + }); + }); + + it("should support promises", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: async function () { + return { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + + describe("with `client_id` in the request query", function () { + it("should return a client", function () { + const client = { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return client; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: "code" }, + headers: {}, + method: {}, + query: { client_id: 12345 }, + }); + + return handler + .getClient(request) + .then(function (data) { + data.should.equal(client); + }) + .catch(should.fail); + }); + }); + }); + + describe("getScope()", function () { + it("should throw an error if `scope` is invalid", async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { scope: "øå€£‰" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getScope(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal("Invalid parameter: `scope`"); + } + }); + + describe("with `scope` in the request body", function () { + it("should return the scope", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { scope: "foo" }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getScope(request).should.eql(["foo"]); + }); + }); + + describe("with `scope` in the request query", function () { + it("should return the scope", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { scope: "foo" }, + }); + + handler.getScope(request).should.eql(["foo"]); + }); + }); + }); + + describe("getState()", function () { + it("should throw an error if `allowEmptyState` is false and `state` is missing", async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + allowEmptyState: false, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getState(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Missing parameter: `state`"); + } + }); + + it("should allow missing `state` if `allowEmptyState` is valid", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + allowEmptyState: true, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + const state = handler.getState(request); + should.equal(state, undefined); + }); + + it("should throw an error if `state` is invalid", async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { state: "øå€£‰" }, + }); + + try { + await handler.getState(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `state`"); + } + }); + + describe("with `state` in the request body", function () { + it("should return the state", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { state: "foobar" }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getState(request).should.equal("foobar"); + }); + }); + + describe("with `state` in the request query", function () { + it("should return the state", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { state: "foobar" }, + }); + + handler.getState(request).should.equal("foobar"); + }); + }); + }); + + describe("getUser()", function () { + it("should throw an error if `user` is missing", function () { + const authenticateHandler = { handle: function () {} }; + const model = Model.from({ + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authenticateHandler: authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + const response = new Response(); + + return handler + .getUser(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + "Server error: `handle()` did not return a `user` object", + ); + }); + }); + + it("should return a user", function () { + const user = {}; + const model = Model.from({ + getAccessToken: function () { + return { + user: user, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getUser(request, response) + .then(function (data) { + data.should.equal(user); + }) + .catch(should.fail); + }); + }); + + describe("saveAuthorizationCode()", function () { + it("should return an auth code", function () { + const authorizationCode = {}; + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .saveAuthorizationCode("foo", "bar", "biz", "baz") + .then(function (data) { + data.should.equal(authorizationCode); + }) + .catch(should.fail); + }); + + it("should support promises when calling `model.saveAuthorizationCode()`", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: async function () { + return {}; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .saveAuthorizationCode("foo", "bar", "biz", "baz") + .should.be.an.instanceOf(Promise); + }); + + it("should support non-promises when calling `model.saveAuthorizationCode()`", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () { + return {}; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .saveAuthorizationCode("foo", "bar", "biz", "baz") + .should.be.an.instanceOf(Promise); + }); + }); + + describe("getResponseType()", function () { + it("should throw an error if `response_type` is missing", async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getResponseType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Missing parameter: `response_type`"); + } + }); + + it("should throw an error if `response_type` is not `code`", async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: "foobar" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getResponseType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedResponseTypeError); + e.message.should.equal( + "Unsupported response type: `response_type` is not supported", + ); + } + }); + + describe("with `response_type` in the request body", function () { + it("should return a response type", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: "code" }, + headers: {}, + method: {}, + query: {}, + }); + const ResponseType = handler.getResponseType(request); + + ResponseType.should.equal(CodeResponseType); + }); + }); + + describe("with `response_type` in the request query", function () { + it("should return a response type", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { response_type: "code" }, + }); + const ResponseType = handler.getResponseType(request); + + ResponseType.should.equal(CodeResponseType); + }); + }); + }); + + describe("buildSuccessRedirectUri()", function () { + it("should return a redirect uri", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType(12345); + const redirectUri = handler.buildSuccessRedirectUri( + "http://example.com/cb", + responseType, + ); + + url.format(redirectUri).should.equal("http://example.com/cb?code=12345"); + }); + }); + + describe("buildErrorRedirectUri()", function () { + it("should set `error_description` if available", function () { + const error = new InvalidClientError("foo bar"); + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const redirectUri = handler.buildErrorRedirectUri( + "http://example.com/cb", + error, + ); + + url + .format(redirectUri) + .should.equal( + "http://example.com/cb?error=invalid_client&error_description=foo%20bar", + ); + }); + + it("should return a redirect uri", function () { + const error = new InvalidClientError(); + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const redirectUri = handler.buildErrorRedirectUri( + "http://example.com/cb", + error, + ); + + url + .format(redirectUri) + .should.equal( + "http://example.com/cb?error=invalid_client&error_description=Bad%20Request", + ); + }); + }); + + describe("updateResponse()", function () { + it("should set the `location` header", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const response = new Response({ body: {}, headers: {} }); + const uri = url.parse("http://example.com/cb"); + + handler.updateResponse(response, uri, "foobar"); + + response + .get("location") + .should.equal("http://example.com/cb?state=foobar"); + }); + }); + + describe("getCodeChallengeMethod()", function () { + it("should throw if the code challenge method is not supported", async function () { + const methods = ["plain", "foo", " ", "0", true, {}, []]; + + for (const method of methods) { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { code_challenge_method: method }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getCodeChallengeMethod(request); + should.fail(); + } catch (e) { + if (method === "plain") { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } else { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + `Invalid request: transform algorithm '${method}' not supported`, + ); + } + } + } + }); + + it("should get code challenge method", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { code_challenge_method: "S256" }, + headers: {}, + method: {}, + query: {}, + }); + + const codeChallengeMethod = handler.getCodeChallengeMethod(request); + codeChallengeMethod.should.equal("S256"); + }); + + it("should throw if the code challenge method is not supported", async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { code_challenge_method: "foo" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getCodeChallengeMethod(request); + + should.fail(); + } catch (e) { + // defined in RFC 7636 - 4.4 + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: transform algorithm 'foo' not supported", + ); + } + }); + + it("should get default code challenge method S256 if missing (default)", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + const codeChallengeMethod = handler.getCodeChallengeMethod(request); + codeChallengeMethod.should.equal("S256"); + }); + + it("should get default code challenge method plain if missing and plain PKCE is enabled", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + enablePlainPKCE: true, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + const codeChallengeMethod = handler.getCodeChallengeMethod(request); + codeChallengeMethod.should.equal("plain"); + }); + }); + + describe("getCodeChallenge()", function () { + it("should get code challenge", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { code_challenge: "challenge" }, + headers: {}, + method: {}, + query: {}, + }); + + const codeChallengeMethod = handler.getCodeChallenge(request); + codeChallengeMethod.should.equal("challenge"); + }); + }); }); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 7fc0a2b4..072f0ced 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -1,1282 +1,1880 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AccessDeniedError = require('../../../lib/errors/access-denied-error'); -const BearerTokenType = require('../../../lib/token-types/bearer-token-type'); -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const InvalidClientError = require('../../../lib/errors/invalid-client-error'); -const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); -const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -const PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); -const Model = require('../../../lib/model'); -const Request = require('../../../lib/request'); -const Response = require('../../../lib/response'); -const ServerError = require('../../../lib/errors/server-error'); -const TokenHandler = require('../../../lib/handlers/token-handler'); -const UnauthorizedClientError = require('../../../lib/errors/unauthorized-client-error'); -const UnsupportedGrantTypeError = require('../../../lib/errors/unsupported-grant-type-error'); -const should = require('chai').should(); -const util = require('util'); -const crypto = require('crypto'); -const stringUtil = require('../../../lib/utils/string-util'); +const AccessDeniedError = require("../../../lib/errors/access-denied-error"); +const BearerTokenType = require("../../../lib/token-types/bearer-token-type"); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const InvalidClientError = require("../../../lib/errors/invalid-client-error"); +const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); +const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); +const PasswordGrantType = require("../../../lib/grant-types/password-grant-type"); +const Model = require("../../../lib/model"); +const Request = require("../../../lib/request"); +const Response = require("../../../lib/response"); +const ServerError = require("../../../lib/errors/server-error"); +const TokenHandler = require("../../../lib/handlers/token-handler"); +const UnauthorizedClientError = require("../../../lib/errors/unauthorized-client-error"); +const UnsupportedGrantTypeError = require("../../../lib/errors/unsupported-grant-type-error"); +const should = require("chai").should(); +const util = require("util"); +const crypto = require("crypto"); +const stringUtil = require("../../../lib/utils/string-util"); /** * Test `TokenHandler` integration. */ -describe('TokenHandler integration', function() { - describe('constructor()', function() { - it('should throw an error if `options.accessTokenLifetime` is missing', function() { - try { - new TokenHandler(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessTokenLifetime`'); - } - }); - - it('should throw an error if `options.model` is missing', function() { - try { - new TokenHandler({ accessTokenLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - - it('should throw an error if `options.refreshTokenLifetime` is missing', function() { - try { - new TokenHandler({ accessTokenLifetime: 120, model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `refreshTokenLifetime`'); - } - }); - - it('should throw an error if the model does not implement `getClient()`', function() { - try { - new TokenHandler({ accessTokenLifetime: 120, model: {}, refreshTokenLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: model does not implement `getClient()`'); - } - }); - - it('should set the `accessTokenLifetime`', function() { - const accessTokenLifetime = {}; - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: accessTokenLifetime, model: model, refreshTokenLifetime: 120 }); - - handler.accessTokenLifetime.should.equal(accessTokenLifetime); - }); - - it('should set the `alwaysIssueNewRefreshToken`', function() { - const alwaysIssueNewRefreshToken = true; - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); - - handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); - }); - - it('should set the `alwaysIssueNewRefreshToken` to false', function() { - const alwaysIssueNewRefreshToken = false; - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); - - handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); - }); - - it('should return the default `alwaysIssueNewRefreshToken` value', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120 }); - - handler.alwaysIssueNewRefreshToken.should.equal(true); - }); - - it('should set the `extendedGrantTypes`', function() { - const extendedGrantTypes = { foo: 'bar' }; - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, extendedGrantTypes: extendedGrantTypes, model: model, refreshTokenLifetime: 120 }); - handler.grantTypes.should.deep.include(extendedGrantTypes); - }); - - it('should set the `model`', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.model.should.equal(model); - }); - - it('should set the `refreshTokenLifetime`', function() { - const refreshTokenLifetime = {}; - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: refreshTokenLifetime }); - - handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); - }); - }); - - describe('handle()', function() { - it('should throw an error if `request` is missing', async function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - try { - await handler.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `request` must be an instance of Request'); - } - }); - - it('should throw an error if `response` is missing', async function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await handler.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid argument: `response` must be an instance of Response'); - } - }); - - it('should throw an error if the method is not `POST`', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: {}, headers: {}, method: 'GET', query: {} }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: method must be POST'); - }); - }); - - it('should throw an error if the media type is not `application/x-www-form-urlencoded`', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: {}, headers: {}, method: 'POST', query: {} }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid request: content must be application/x-www-form-urlencoded'); - }); - }); - - it('should throw the error if an oauth error is thrown', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: {}, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: cannot retrieve client credentials'); - }); - }); - - it('should throw a server error if a non-oauth error is thrown', function() { - const model = Model.from({ - getClient: function() { - throw new Error('Unhandled exception'); - }, - getUser: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - grant_type: 'password', - password: 'bar', - username: 'foo' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Unhandled exception'); - e.inner.should.be.an.instanceOf(Error); - }); - }); - - it('should update the response if an error is thrown', function() { - const model = Model.from({ - getClient: function() { - throw new Error('Unhandled exception'); - }, - getUser: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - grant_type: 'password', - password: 'bar', - username: 'foo' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(should.fail) - .catch(function() { - response.body.should.eql({ error: 'server_error', error_description: 'Unhandled exception' }); - response.status.should.equal(503); - }); - }); - - it('should return a bearer token if successful', function() { - const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: ['foobar'], user: {} }; - const model = Model.from({ - getClient: function() { return { grants: ['password'] }; }, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return ['baz']; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - username: 'foo', - password: 'bar', - grant_type: 'password', - scope: 'baz' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function(data) { - data.should.eql(token); - }) - .catch(should.fail); - }); - - it('should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set', function() { - const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: ['baz'], user: {}, foo: 'bar' }; - const model = Model.from({ - getClient: function() { return { grants: ['password'] }; }, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return ['baz']; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - username: 'foo', - password: 'bar', - grant_type: 'password', - scope: 'baz' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - should.exist(response.body.access_token); - should.exist(response.body.refresh_token); - should.exist(response.body.token_type); - response.body.scope.should.eql('baz'); - should.not.exist(response.body.foo); - }) - .catch(should.fail); - }); - - it('should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set', function() { - const token = { accessToken: 'foo', client: {}, refreshToken: 'bar', scope: ['baz'], user: {}, foo: 'bar' }; - const model = Model.from({ - getClient: function() { return { grants: ['password'] }; }, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return ['baz']; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, allowExtendedTokenAttributes: true }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - username: 'foo', - password: 'bar', - grant_type: 'password', - scope: 'baz' - }, - headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, - method: 'POST', - query: {} - }); - const response = new Response({ body: {}, headers: {} }); - - return handler.handle(request, response) - .then(function() { - should.exist(response.body.access_token); - should.exist(response.body.refresh_token); - should.exist(response.body.token_type); - response.body.scope.should.eql('baz'); - should.exist(response.body.foo); - }) - .catch(should.fail); - }); - }); - - - describe('getClient()', function() { - it('should throw an error if `clientId` is invalid', async function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 'øå€£‰', client_secret: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_id`'); - } - }); - - it('should throw an error if `clientSecret` is invalid', async function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 'foo', client_secret: 'øå€£‰' }, headers: {}, method: {}, query: {} }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `client_secret`'); - } - }); - - it('should throw an error if `client` is missing', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: client is invalid'); - }); - }); - - it('should throw an error if `client.grants` is missing', function() { - const model = Model.from({ - getClient: function() { return {}; }, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: missing client `grants`'); - }); - }); - - it('should throw an error if `client.grants` is invalid', function() { - const model = Model.from({ - getClient: function() { return { grants: 'foobar' }; }, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal('Server error: `grants` must be an array'); - }); - }); - - it('should throw a 401 error if the client is invalid and the request contains an authorization header', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: {}, - headers: { 'authorization': util.format('Basic %s', Buffer.from('foo:bar').toString('base64')) }, - method: {}, - query: {} - }); - const response = new Response({ body: {}, headers: {} }); - - return handler.getClient(request, response) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidClientError); - e.code.should.equal(401); - e.message.should.equal('Invalid client: client is invalid'); - - response.get('WWW-Authenticate').should.equal('Basic realm="Service"'); - }); - }); - - it('should return a client', function() { - const client = { id: 12345, grants: [] }; - const model = Model.from({ - getClient: function() { return client; }, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - - describe('with `password` grant type and `requireClientAuthentication` is false', function() { - - it('should return a client ', function() { - const client = { id: 12345, grants: [] }; - const model = Model.from({ - getClient: function() { return client; }, - saveToken: function() {} - }); - - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - requireClientAuthentication: { - password: false - } - }); - const request = new Request({ body: { client_id: 'blah', grant_type: 'password'}, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - - describe('with `password` grant type and `requireClientAuthentication` is false and Authorization header', function() { - - it('should return a client ', function() { - const client = { id: 12345, grants: [] }; - const model = Model.from({ - getClient: function() { return client; }, - saveToken: function() {} - }); - - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - requireClientAuthentication: { - password: false - } - }); - const request = new Request({ - body: { grant_type: 'password'}, - headers: { 'authorization': util.format('Basic %s', Buffer.from('blah:').toString('base64')) }, - method: {}, - query: {} - }); - - return handler.getClient(request) - .then(function(data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - - it('should support promises', function() { - const model = Model.from({ - getClient: async function() { return { grants: [] }; }, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it('should support non-promises', function() { - const model = Model.from({ - getClient: function() { return { grants: [] }; }, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - }); - - describe('getClientCredentials()', function() { - it('should throw an error if `client_id` is missing', async function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_secret: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - await handler.getClientCredentials(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: cannot retrieve client credentials'); - } - }); - - it('should throw an error if `client_secret` is missing', async function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 'foo' }, headers: {}, method: {}, query: {} }); - - try { - await handler.getClientCredentials(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal('Invalid client: cannot retrieve client credentials'); - } - }); - - describe('with `client_id` and grant type is `password` and `requireClientAuthentication` is false', function() { - it('should return a client', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, requireClientAuthentication: { password: false} }); - const request = new Request({ body: { client_id: 'foo', grant_type: 'password' }, headers: {}, method: {}, query: {} }); - const credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: 'foo' }); - }); - }); - - describe('with `client_id` and `client_secret` in the request header as basic auth', function() { - it('should return a client', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: {}, - headers: { - 'authorization': util.format('Basic %s', Buffer.from('foo:bar').toString('base64')) - }, - method: {}, - query: {} - }); - const credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); - }); - }); - - describe('with `client_id` and `client_secret` in the request body', function() { - it('should return a client', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 'foo', client_secret: 'bar' }, headers: {}, method: {}, query: {} }); - const credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); - }); - }); - }); - - describe('handleGrantType()', function() { - it('should throw an error if `grant_type` is missing', async function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - try { - await handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Missing parameter: `grant_type`'); - } - }); - - it('should throw an error if `grant_type` is invalid', async function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { grant_type: '~foo~' }, headers: {}, method: {}, query: {} }); - - try { - await handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal('Invalid parameter: `grant_type`'); - } - }); - - it('should throw an error if `grant_type` is unsupported', async function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { grant_type: 'foobar' }, headers: {}, method: {}, query: {} }); - - try { - await handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedGrantTypeError); - e.message.should.equal('Unsupported grant type: `grant_type` is invalid'); - } - }); - - it('should throw an error if `grant_type` is unauthorized', async function() { - const client = { grants: ['client_credentials'] }; - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { grant_type: 'password' }, headers: {}, method: {}, query: {} }); - - try { - await handler.handleGrantType(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal('Unauthorized client: `grant_type` is invalid'); - } - }); - - it('should throw an invalid grant error if a non-oauth error is thrown', function() { - const client = { grants: ['password'] }; - const model = Model.from({ - getClient: function(clientId, password) { return client; }, - getUser: function(uid, pwd) {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { grant_type: 'password', username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return handler.handleGrantType(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: user credentials are invalid'); - }); - }); - - describe('with grant_type `authorization_code`', function() { - it('should return a token', function() { - const client = { id: 'foobar', grants: ['authorization_code'] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), user: {} }; }, - getClient: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return ['foo']; }, - revokeAuthorizationCode: function() { return { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), user: {} }; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - code: 12345, - grant_type: 'authorization_code' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with PKCE', function() { - it('should return a token when code verifier is valid using S256 code challenge method', async function() { - const methods = ['S256', undefined]; - - for (const method of methods) { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: 'foobar' }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: method, - codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) - }; - const client = { id: 'foobar', grants: ['authorization_code'] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - getClient: function () { - }, - saveToken: function () { - return token; - }, - validateScope: function () { - return ['foo']; - }, - revokeAuthorizationCode: function () { - return authorizationCode; - } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - code: 12345, - grant_type: 'authorization_code', - code_verifier: codeVerifier - }, - headers: {}, - method: {}, - query: {} - }); - - const data = await handler.handleGrantType(request, client); - data.should.equal(token); - } - }); - - it('should return a token when code verifier is valid using plain code challenge method', async function() { - const methods = ['plain', undefined]; - - for (const method of methods) { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: 'foobar' }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: method, - codeChallenge: codeVerifier - }; - const client = { id: 'foobar', grants: ['authorization_code'] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - getClient: function () { - }, - saveToken: function () { - return token; - }, - validateScope: function () { - return ['foo']; - }, - revokeAuthorizationCode: function () { - return authorizationCode; - } - }); - const handler = new TokenHandler({ - enablePlainPKCE: true, - accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 - }); - const request = new Request({ - body: { - code: 12345, - grant_type: 'authorization_code', - code_verifier: codeVerifier - }, - headers: {}, - method: {}, - query: {} - }); - - const data = await handler.handleGrantType(request, client); - data.should.equal(token); - } - }); - - it('should throw an invalid grant error when code verifier is invalid', function() { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: 'foobar' }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: 'S256', - codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) - }; - const client = { id: 'foobar', grants: ['authorization_code'] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function() { return authorizationCode; }, - getClient: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return ['foo']; }, - revokeAuthorizationCode: function() { return authorizationCode; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - code: 12345, - grant_type: 'authorization_code', - code_verifier: '123123123123123123123123123123123123123123123' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: code verifier is invalid'); - }); - }); - - it('should throw an invalid grant error when code verifier is missing', function() { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: 'foobar' }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: 'S256', - codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) - }; - const client = { id: 'foobar', grants: ['authorization_code'] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function() { return authorizationCode; }, - getClient: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return ['foo']; }, - revokeAuthorizationCode: function() { return authorizationCode; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - code: 12345, - grant_type: 'authorization_code' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Missing parameter: `code_verifier`'); - }); - }); - - it('should throw an invalid grant error when code verifier is present but code challenge is missing', function() { - const authorizationCode = { - authorizationCode: 12345, - client: { id: 'foobar' }, - expiresAt: new Date(new Date().getTime() * 2), - user: {} - }; - const client = { id: 'foobar', grants: ['authorization_code'] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function() { return authorizationCode; }, - getClient: function() {}, - saveToken: function() { return token; }, - validateScope: function() { return ['foo']; }, - revokeAuthorizationCode: function() { return authorizationCode; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - code: 12345, - grant_type: 'authorization_code', - code_verifier: '123123123123123123123123123123123123123123123' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(should.fail) - .catch(function(e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal('Invalid grant: code verifier is invalid'); - }); - }); - }); - - describe('with grant_type `client_credentials`', function() { - it('should return a token', function() { - const client = { grants: ['client_credentials'] }; - const token = {}; - const model = Model.from({ - getClient: function() {}, - getUserFromClient: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return ['foo']; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - grant_type: 'client_credentials', - scope: 'foo' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `password`', function() { - it('should return a token', function() { - const client = { grants: ['password'] }; - const token = {}; - const model = Model.from({ - getClient: function() {}, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return ['baz']; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: 'secret', - grant_type: 'password', - password: 'bar', - username: 'foo', - scope: 'baz' - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with grant_type `refresh_token`', function() { - it('should return a token', function() { - const client = { grants: ['refresh_token'] }; - const token = { accessToken: 'foo', client: {}, user: {} }; - const model = Model.from({ - getClient: function() {}, - getRefreshToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), user: {} }; }, - saveToken: function() { return token; }, - revokeToken: function() { return { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ - body: { - grant_type: 'refresh_token', - refresh_token: 12345 - }, - headers: {}, - method: {}, - query: {} - }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe('with custom grant_type', function() { - it('should return a token', function() { - const client = { grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'] }; - const token = {}; - const model = Model.from({ - getClient: function() {}, - getUser: function() { return {}; }, - saveToken: function() { return token; }, - validateScope: function() { return ['foo']; } - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, extendedGrantTypes: { 'urn:ietf:params:oauth:grant-type:saml2-bearer': PasswordGrantType } }); - const request = new Request({ body: { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); - - return handler.handleGrantType(request, client) - .then(function(data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - }); - - describe('getAccessTokenLifetime()', function() { - it('should return the client access token lifetime', function() { - const client = { accessTokenLifetime: 60 }; - const model = Model.from({ - getClient: function() { return client; }, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getAccessTokenLifetime(client).should.equal(60); - }); - - it('should return the default access token lifetime', function() { - const client = {}; - const model = Model.from({ - getClient: function() { return client; }, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getAccessTokenLifetime(client).should.equal(120); - }); - }); - - describe('getRefreshTokenLifetime()', function() { - it('should return the client access token lifetime', function() { - const client = { refreshTokenLifetime: 60 }; - const model = Model.from({ - getClient: function() { return client; }, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getRefreshTokenLifetime(client).should.equal(60); - }); - - it('should return the default access token lifetime', function() { - const client = {}; - const model = Model.from({ - getClient: function() { return client; }, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - - handler.getRefreshTokenLifetime(client).should.equal(120); - }); - }); - - describe('getTokenType()', function() { - it('should return a token type', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', scope: ['foobar'] }); - tokenType.should.deep.include({ accessToken: 'foo', accessTokenLifetime: undefined, refreshToken: 'bar', scope: ['foobar'] }); - }); - }); - - describe('updateSuccessResponse()', function() { - it('should set the `body`', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const tokenType = new BearerTokenType('foo', 'bar', 'biz'); - const response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.body.should.eql({ access_token: 'foo', expires_in: 'bar', refresh_token: 'biz', token_type: 'Bearer' }); - }); - - it('should set the `Cache-Control` header', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const tokenType = new BearerTokenType('foo', 'bar', 'biz'); - const response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.get('Cache-Control').should.equal('no-store'); - }); - - it('should set the `Pragma` header', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const tokenType = new BearerTokenType('foo', 'bar', 'biz'); - const response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.get('Pragma').should.equal('no-cache'); - }); - }); - - describe('updateErrorResponse()', function() { - it('should set the `body`', function() { - const error = new AccessDeniedError('Cannot request a token'); - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateErrorResponse(response, error); - - response.body.error.should.equal('access_denied'); - response.body.error_description.should.equal('Cannot request a token'); - }); - - it('should set the `status`', function() { - const error = new AccessDeniedError('Cannot request a token'); - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateErrorResponse(response, error); - - response.status.should.equal(400); - }); - }); +describe("TokenHandler integration", function () { + describe("constructor()", function () { + it("should throw an error if `options.accessTokenLifetime` is missing", function () { + try { + new TokenHandler(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `accessTokenLifetime`"); + } + }); + + it("should throw an error if `options.model` is missing", function () { + try { + new TokenHandler({ accessTokenLifetime: 120 }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `model`"); + } + }); + + it("should throw an error if `options.refreshTokenLifetime` is missing", function () { + try { + new TokenHandler({ accessTokenLifetime: 120, model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `refreshTokenLifetime`"); + } + }); + + it("should throw an error if the model does not implement `getClient()`", function () { + try { + new TokenHandler({ + accessTokenLifetime: 120, + model: {}, + refreshTokenLifetime: 120, + }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: model does not implement `getClient()`", + ); + } + }); + + it("should set the `accessTokenLifetime`", function () { + const accessTokenLifetime = {}; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: accessTokenLifetime, + model: model, + refreshTokenLifetime: 120, + }); + + handler.accessTokenLifetime.should.equal(accessTokenLifetime); + }); + + it("should set the `alwaysIssueNewRefreshToken`", function () { + const alwaysIssueNewRefreshToken = true; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 120, + alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, + }); + + handler.alwaysIssueNewRefreshToken.should.equal( + alwaysIssueNewRefreshToken, + ); + }); + + it("should set the `alwaysIssueNewRefreshToken` to false", function () { + const alwaysIssueNewRefreshToken = false; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 120, + alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, + }); + + handler.alwaysIssueNewRefreshToken.should.equal( + alwaysIssueNewRefreshToken, + ); + }); + + it("should return the default `alwaysIssueNewRefreshToken` value", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 120, + }); + + handler.alwaysIssueNewRefreshToken.should.equal(true); + }); + + it("should set the `extendedGrantTypes`", function () { + const extendedGrantTypes = { foo: "bar" }; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + extendedGrantTypes: extendedGrantTypes, + model: model, + refreshTokenLifetime: 120, + }); + handler.grantTypes.should.deep.include(extendedGrantTypes); + }); + + it("should set the `model`", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.model.should.equal(model); + }); + + it("should set the `refreshTokenLifetime`", function () { + const refreshTokenLifetime = {}; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: refreshTokenLifetime, + }); + + handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); + }); + }); + + describe("handle()", function () { + it("should throw an error if `request` is missing", async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + try { + await handler.handle(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: `request` must be an instance of Request", + ); + } + }); + + it("should throw an error if `response` is missing", async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + "Invalid argument: `response` must be an instance of Response", + ); + } + }); + + it("should throw an error if the method is not `POST`", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: "GET", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid request: method must be POST"); + }); + }); + + it("should throw an error if the media type is not `application/x-www-form-urlencoded`", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: "POST", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: content must be application/x-www-form-urlencoded", + ); + }); + }); + + it("should throw the error if an oauth error is thrown", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + "content-type": "application/x-www-form-urlencoded", + "transfer-encoding": "chunked", + }, + method: "POST", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + "Invalid client: cannot retrieve client credentials", + ); + }); + }); + + it("should throw a server error if a non-oauth error is thrown", function () { + const model = Model.from({ + getClient: function () { + throw new Error("Unhandled exception"); + }, + getUser: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: "secret", + grant_type: "password", + password: "bar", + username: "foo", + }, + headers: { + "content-type": "application/x-www-form-urlencoded", + "transfer-encoding": "chunked", + }, + method: "POST", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal("Unhandled exception"); + e.inner.should.be.an.instanceOf(Error); + }); + }); + + it("should update the response if an error is thrown", function () { + const model = Model.from({ + getClient: function () { + throw new Error("Unhandled exception"); + }, + getUser: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: "secret", + grant_type: "password", + password: "bar", + username: "foo", + }, + headers: { + "content-type": "application/x-www-form-urlencoded", + "transfer-encoding": "chunked", + }, + method: "POST", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function () { + response.body.should.eql({ + error: "server_error", + error_description: "Unhandled exception", + }); + response.status.should.equal(503); + }); + }); + + it("should return a bearer token if successful", function () { + const token = { + accessToken: "foo", + client: {}, + refreshToken: "bar", + scope: ["foobar"], + user: {}, + }; + const model = Model.from({ + getClient: function () { + return { grants: ["password"] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["baz"]; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: "secret", + username: "foo", + password: "bar", + grant_type: "password", + scope: "baz", + }, + headers: { + "content-type": "application/x-www-form-urlencoded", + "transfer-encoding": "chunked", + }, + method: "POST", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function (data) { + data.should.eql(token); + }) + .catch(should.fail); + }); + + it("should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set", function () { + const token = { + accessToken: "foo", + client: {}, + refreshToken: "bar", + scope: ["baz"], + user: {}, + foo: "bar", + }; + const model = Model.from({ + getClient: function () { + return { grants: ["password"] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["baz"]; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: "secret", + username: "foo", + password: "bar", + grant_type: "password", + scope: "baz", + }, + headers: { + "content-type": "application/x-www-form-urlencoded", + "transfer-encoding": "chunked", + }, + method: "POST", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function () { + should.exist(response.body.access_token); + should.exist(response.body.refresh_token); + should.exist(response.body.token_type); + response.body.scope.should.eql("baz"); + should.not.exist(response.body.foo); + }) + .catch(should.fail); + }); + + it("should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set", function () { + const token = { + accessToken: "foo", + client: {}, + refreshToken: "bar", + scope: ["baz"], + user: {}, + foo: "bar", + }; + const model = Model.from({ + getClient: function () { + return { grants: ["password"] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["baz"]; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + allowExtendedTokenAttributes: true, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: "secret", + username: "foo", + password: "bar", + grant_type: "password", + scope: "baz", + }, + headers: { + "content-type": "application/x-www-form-urlencoded", + "transfer-encoding": "chunked", + }, + method: "POST", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function () { + should.exist(response.body.access_token); + should.exist(response.body.refresh_token); + should.exist(response.body.token_type); + response.body.scope.should.eql("baz"); + should.exist(response.body.foo); + }) + .catch(should.fail); + }); + }); + + describe("getClient()", function () { + it("should throw an error if `clientId` is invalid", async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: "øå€£‰", client_secret: "foo" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `client_id`"); + } + }); + + it("should throw an error if `clientSecret` is invalid", async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: "foo", client_secret: "øå€£‰" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `client_secret`"); + } + }); + + it("should throw an error if `client` is missing", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal("Invalid client: client is invalid"); + }); + }); + + it("should throw an error if `client.grants` is missing", function () { + const model = Model.from({ + getClient: function () { + return {}; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal("Server error: missing client `grants`"); + }); + }); + + it("should throw an error if `client.grants` is invalid", function () { + const model = Model.from({ + getClient: function () { + return { grants: "foobar" }; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal("Server error: `grants` must be an array"); + }); + }); + + it("should throw a 401 error if the client is invalid and the request contains an authorization header", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + "Basic %s", + Buffer.from("foo:bar").toString("base64"), + ), + }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getClient(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.code.should.equal(401); + e.message.should.equal("Invalid client: client is invalid"); + + response + .get("WWW-Authenticate") + .should.equal('Basic realm="Service"'); + }); + }); + + it("should return a client", function () { + const client = { id: 12345, grants: [] }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(function (data) { + data.should.equal(client); + }) + .catch(should.fail); + }); + + describe("with `password` grant type and `requireClientAuthentication` is false", function () { + it("should return a client ", function () { + const client = { id: 12345, grants: [] }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + requireClientAuthentication: { + password: false, + }, + }); + const request = new Request({ + body: { client_id: "blah", grant_type: "password" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(function (data) { + data.should.equal(client); + }) + .catch(should.fail); + }); + }); + + describe("with `password` grant type and `requireClientAuthentication` is false and Authorization header", function () { + it("should return a client ", function () { + const client = { id: 12345, grants: [] }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + requireClientAuthentication: { + password: false, + }, + }); + const request = new Request({ + body: { grant_type: "password" }, + headers: { + authorization: util.format( + "Basic %s", + Buffer.from("blah:").toString("base64"), + ), + }, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(function (data) { + data.should.equal(client); + }) + .catch(should.fail); + }); + }); + + it("should support promises", function () { + const model = Model.from({ + getClient: async function () { + return { grants: [] }; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret" }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + + it("should support non-promises", function () { + const model = Model.from({ + getClient: function () { + return { grants: [] }; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret" }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + }); + + describe("getClientCredentials()", function () { + it("should throw an error if `client_id` is missing", async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_secret: "foo" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClientCredentials(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + "Invalid client: cannot retrieve client credentials", + ); + } + }); + + it("should throw an error if `client_secret` is missing", async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: "foo" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClientCredentials(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + "Invalid client: cannot retrieve client credentials", + ); + } + }); + + describe("with `client_id` and grant type is `password` and `requireClientAuthentication` is false", function () { + it("should return a client", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + requireClientAuthentication: { password: false }, + }); + const request = new Request({ + body: { client_id: "foo", grant_type: "password" }, + headers: {}, + method: {}, + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: "foo" }); + }); + }); + + describe("with `client_id` and `client_secret` in the request header as basic auth", function () { + it("should return a client", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + "Basic %s", + Buffer.from("foo:bar").toString("base64"), + ), + }, + method: {}, + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: "foo", clientSecret: "bar" }); + }); + }); + + describe("with `client_id` and `client_secret` in the request body", function () { + it("should return a client", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: "foo", client_secret: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: "foo", clientSecret: "bar" }); + }); + }); + }); + + describe("handleGrantType()", function () { + it("should throw an error if `grant_type` is missing", async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handleGrantType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Missing parameter: `grant_type`"); + } + }); + + it("should throw an error if `grant_type` is invalid", async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: "~foo~" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handleGrantType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal("Invalid parameter: `grant_type`"); + } + }); + + it("should throw an error if `grant_type` is unsupported", async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: "foobar" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handleGrantType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedGrantTypeError); + e.message.should.equal( + "Unsupported grant type: `grant_type` is invalid", + ); + } + }); + + it("should throw an error if `grant_type` is unauthorized", async function () { + const client = { grants: ["client_credentials"] }; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: "password" }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handleGrantType(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnauthorizedClientError); + e.message.should.equal("Unauthorized client: `grant_type` is invalid"); + } + }); + + it("should throw an invalid grant error if a non-oauth error is thrown", function () { + const client = { grants: ["password"] }; + const model = Model.from({ + getClient: function (clientId, password) { + return client; + }, + getUser: function (uid, pwd) {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: "password", username: "foo", password: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: user credentials are invalid"); + }); + }); + + describe("with grant_type `authorization_code`", function () { + it("should return a token", function () { + const client = { id: "foobar", grants: ["authorization_code"] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["foo"]; + }, + revokeAuthorizationCode: function () { + return { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: "authorization_code", + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe("with PKCE", function () { + it("should return a token when code verifier is valid using S256 code challenge method", async function () { + const methods = ["S256", undefined]; + + for (const method of methods) { + const codeVerifier = stringUtil.base64URLEncode( + crypto.randomBytes(32), + ); + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: method, + codeChallenge: stringUtil.base64URLEncode( + crypto.createHash("sha256").update(codeVerifier).digest(), + ), + }; + const client = { id: "foobar", grants: ["authorization_code"] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["foo"]; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: "authorization_code", + code_verifier: codeVerifier, + }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await handler.handleGrantType(request, client); + data.should.equal(token); + } + }); + + it("should return a token when code verifier is valid using plain code challenge method", async function () { + const methods = ["plain", undefined]; + + for (const method of methods) { + const codeVerifier = stringUtil.base64URLEncode( + crypto.randomBytes(32), + ); + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: method, + codeChallenge: codeVerifier, + }; + const client = { id: "foobar", grants: ["authorization_code"] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["foo"]; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + enablePlainPKCE: true, + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: "authorization_code", + code_verifier: codeVerifier, + }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await handler.handleGrantType(request, client); + data.should.equal(token); + } + }); + + it("should throw an invalid grant error when code verifier is invalid", function () { + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: "S256", + codeChallenge: stringUtil.base64URLEncode( + crypto.createHash("sha256").update(codeVerifier).digest(), + ), + }; + const client = { id: "foobar", grants: ["authorization_code"] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["foo"]; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: "authorization_code", + code_verifier: "123123123123123123123123123123123123123123123", + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: code verifier is invalid"); + }); + }); + + it("should throw an invalid grant error when code verifier is missing", function () { + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: "S256", + codeChallenge: stringUtil.base64URLEncode( + crypto.createHash("sha256").update(codeVerifier).digest(), + ), + }; + const client = { id: "foobar", grants: ["authorization_code"] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["foo"]; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: "authorization_code", + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Missing parameter: `code_verifier`"); + }); + }); + + it("should throw an invalid grant error when code verifier is present but code challenge is missing", function () { + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + const client = { id: "foobar", grants: ["authorization_code"] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["foo"]; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: "authorization_code", + code_verifier: "123123123123123123123123123123123123123123123", + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal("Invalid grant: code verifier is invalid"); + }); + }); + }); + + describe("with grant_type `client_credentials`", function () { + it("should return a token", function () { + const client = { grants: ["client_credentials"] }; + const token = {}; + const model = Model.from({ + getClient: function () {}, + getUserFromClient: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["foo"]; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + grant_type: "client_credentials", + scope: "foo", + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe("with grant_type `password`", function () { + it("should return a token", function () { + const client = { grants: ["password"] }; + const token = {}; + const model = Model.from({ + getClient: function () {}, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["baz"]; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: "secret", + grant_type: "password", + password: "bar", + username: "foo", + scope: "baz", + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe("with grant_type `refresh_token`", function () { + it("should return a token", function () { + const client = { grants: ["refresh_token"] }; + const token = { accessToken: "foo", client: {}, user: {} }; + const model = Model.from({ + getClient: function () {}, + getRefreshToken: function () { + return { + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + saveToken: function () { + return token; + }, + revokeToken: function () { + return { + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + grant_type: "refresh_token", + refresh_token: 12345, + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe("with custom grant_type", function () { + it("should return a token", function () { + const client = { + grants: ["urn:ietf:params:oauth:grant-type:saml2-bearer"], + }; + const token = {}; + const model = Model.from({ + getClient: function () {}, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ["foo"]; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + extendedGrantTypes: { + "urn:ietf:params:oauth:grant-type:saml2-bearer": PasswordGrantType, + }, + }); + const request = new Request({ + body: { + grant_type: "urn:ietf:params:oauth:grant-type:saml2-bearer", + username: "foo", + password: "bar", + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + }); + + describe("getAccessTokenLifetime()", function () { + it("should return the client access token lifetime", function () { + const client = { accessTokenLifetime: 60 }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.getAccessTokenLifetime(client).should.equal(60); + }); + + it("should return the default access token lifetime", function () { + const client = {}; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.getAccessTokenLifetime(client).should.equal(120); + }); + }); + + describe("getRefreshTokenLifetime()", function () { + it("should return the client access token lifetime", function () { + const client = { refreshTokenLifetime: 60 }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.getRefreshTokenLifetime(client).should.equal(60); + }); + + it("should return the default access token lifetime", function () { + const client = {}; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.getRefreshTokenLifetime(client).should.equal(120); + }); + }); + + describe("getTokenType()", function () { + it("should return a token type", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const tokenType = handler.getTokenType({ + accessToken: "foo", + refreshToken: "bar", + scope: ["foobar"], + }); + tokenType.should.deep.include({ + accessToken: "foo", + accessTokenLifetime: undefined, + refreshToken: "bar", + scope: ["foobar"], + }); + }); + }); + + describe("updateSuccessResponse()", function () { + it("should set the `body`", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType("foo", "bar", "biz"); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.body.should.eql({ + access_token: "foo", + expires_in: "bar", + refresh_token: "biz", + token_type: "Bearer", + }); + }); + + it("should set the `Cache-Control` header", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType("foo", "bar", "biz"); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.get("Cache-Control").should.equal("no-store"); + }); + + it("should set the `Pragma` header", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType("foo", "bar", "biz"); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.get("Pragma").should.equal("no-cache"); + }); + }); + + describe("updateErrorResponse()", function () { + it("should set the `body`", function () { + const error = new AccessDeniedError("Cannot request a token"); + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.body.error.should.equal("access_denied"); + response.body.error_description.should.equal("Cannot request a token"); + }); + + it("should set the `status`", function () { + const error = new AccessDeniedError("Cannot request a token"); + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.status.should.equal(400); + }); + }); }); diff --git a/test/integration/request_test.js b/test/integration/request_test.js index e0c98f13..2b6bd9ba 100644 --- a/test/integration/request_test.js +++ b/test/integration/request_test.js @@ -1,159 +1,189 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const Request = require('../../lib/request'); -const InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); -const should = require('chai').should(); +const Request = require("../../lib/request"); +const InvalidArgumentError = require("../../lib/errors/invalid-argument-error"); +const should = require("chai").should(); /** * Test `Request` integration. */ -describe('Request integration', function() { - describe('constructor()', function() { - it('should throw an error if `headers` is missing', function() { - try { - new Request({ body: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `headers`'); - } - }); - - it('should throw an error if `method` is missing', function() { - try { - new Request({ body: {}, headers: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `method`'); - } - }); - - it('should throw an error if `query` is missing', function() { - try { - new Request({ body: {}, headers: {}, method: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `query`'); - } - }); - - it('should set the `body`', function() { - const request = new Request({ body: 'foo', headers: {}, method: {}, query: {} }); - - request.body.should.equal('foo'); - }); - - it('should set the `headers`', function() { - const request = new Request({ body: {}, headers: { foo: 'bar', QuX: 'biz' }, method: {}, query: {} }); - - request.headers.should.eql({ foo: 'bar', qux: 'biz' }); - }); - - it('should set the `method`', function() { - const request = new Request({ body: {}, headers: {}, method: 'biz', query: {} }); - - request.method.should.equal('biz'); - }); - - it('should set the `query`', function() { - const request = new Request({ body: {}, headers: {}, method: {}, query: 'baz' }); - - request.query.should.equal('baz'); - }); - }); - - describe('get()', function() { - it('should return `undefined` if the field does not exist', function() { - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - (undefined === request.get('content-type')).should.be.true; - }); - - it('should return the value if the field exists', function() { - const request = new Request({ - body: {}, - headers: { - 'content-type': 'text/html; charset=utf-8' - }, - method: {}, - query: {} - }); - - request.get('Content-Type').should.equal('text/html; charset=utf-8'); - }); - }); - - describe('is()', function() { - it('should accept an array of `types`', function() { - const request = new Request({ - body: {}, - headers: { - 'content-type': 'application/json', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is(['html', 'json']).should.equal('json'); - }); - - it('should accept multiple `types` as arguments', function() { - const request = new Request({ - body: {}, - headers: { - 'content-type': 'application/json', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is('html', 'json').should.equal('json'); - }); - - it('should return the first matching type', function() { - const request = new Request({ - body: {}, - headers: { - 'content-type': 'text/html; charset=utf-8', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is('html').should.equal('html'); - }); - - it('should return `false` if none of the `types` match', function() { - const request = new Request({ - body: {}, - headers: { - 'content-type': 'text/html; charset=utf-8', - 'transfer-encoding': 'chunked' - }, - method: {}, - query: {} - }); - - request.is('json').should.be.false; - }); - - it('should return `false` if the request has no body', function() { - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - - request.is('text/html').should.be.false; - }); - }); +describe("Request integration", function () { + describe("constructor()", function () { + it("should throw an error if `headers` is missing", function () { + try { + new Request({ body: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `headers`"); + } + }); + + it("should throw an error if `method` is missing", function () { + try { + new Request({ body: {}, headers: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `method`"); + } + }); + + it("should throw an error if `query` is missing", function () { + try { + new Request({ body: {}, headers: {}, method: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `query`"); + } + }); + + it("should set the `body`", function () { + const request = new Request({ + body: "foo", + headers: {}, + method: {}, + query: {}, + }); + + request.body.should.equal("foo"); + }); + + it("should set the `headers`", function () { + const request = new Request({ + body: {}, + headers: { foo: "bar", QuX: "biz" }, + method: {}, + query: {}, + }); + + request.headers.should.eql({ foo: "bar", qux: "biz" }); + }); + + it("should set the `method`", function () { + const request = new Request({ + body: {}, + headers: {}, + method: "biz", + query: {}, + }); + + request.method.should.equal("biz"); + }); + + it("should set the `query`", function () { + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: "baz", + }); + + request.query.should.equal("baz"); + }); + }); + + describe("get()", function () { + it("should return `undefined` if the field does not exist", function () { + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + (undefined === request.get("content-type")).should.be.true; + }); + + it("should return the value if the field exists", function () { + const request = new Request({ + body: {}, + headers: { + "content-type": "text/html; charset=utf-8", + }, + method: {}, + query: {}, + }); + + request.get("Content-Type").should.equal("text/html; charset=utf-8"); + }); + }); + + describe("is()", function () { + it("should accept an array of `types`", function () { + const request = new Request({ + body: {}, + headers: { + "content-type": "application/json", + "transfer-encoding": "chunked", + }, + method: {}, + query: {}, + }); + + request.is(["html", "json"]).should.equal("json"); + }); + + it("should accept multiple `types` as arguments", function () { + const request = new Request({ + body: {}, + headers: { + "content-type": "application/json", + "transfer-encoding": "chunked", + }, + method: {}, + query: {}, + }); + + request.is("html", "json").should.equal("json"); + }); + + it("should return the first matching type", function () { + const request = new Request({ + body: {}, + headers: { + "content-type": "text/html; charset=utf-8", + "transfer-encoding": "chunked", + }, + method: {}, + query: {}, + }); + + request.is("html").should.equal("html"); + }); + + it("should return `false` if none of the `types` match", function () { + const request = new Request({ + body: {}, + headers: { + "content-type": "text/html; charset=utf-8", + "transfer-encoding": "chunked", + }, + method: {}, + query: {}, + }); + + request.is("json").should.be.false; + }); + + it("should return `false` if the request has no body", function () { + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + request.is("text/html").should.be.false; + }); + }); }); diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js index 44bd53b0..c42f765a 100644 --- a/test/integration/response-types/code-response-type_test.js +++ b/test/integration/response-types/code-response-type_test.js @@ -1,64 +1,70 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const CodeResponseType = require('../../../lib/response-types/code-response-type'); -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const should = require('chai').should(); -const url = require('url'); +const CodeResponseType = require("../../../lib/response-types/code-response-type"); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const should = require("chai").should(); +const url = require("url"); /** * Test `CodeResponseType` integration. */ -describe('CodeResponseType integration', function() { - describe('constructor()', function() { - it('should throw an error if `code` is missing', function() { - try { - new CodeResponseType(); +describe("CodeResponseType integration", function () { + describe("constructor()", function () { + it("should throw an error if `code` is missing", function () { + try { + new CodeResponseType(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `code`'); - } - }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `code`"); + } + }); - it('should set the `code`', function() { - const responseType = new CodeResponseType('foo'); + it("should set the `code`", function () { + const responseType = new CodeResponseType("foo"); - responseType.code.should.equal('foo'); - }); - }); + responseType.code.should.equal("foo"); + }); + }); - describe('buildRedirectUri()', function() { - it('should throw an error if the `redirectUri` is missing', function() { - const responseType = new CodeResponseType('foo'); + describe("buildRedirectUri()", function () { + it("should throw an error if the `redirectUri` is missing", function () { + const responseType = new CodeResponseType("foo"); - try { - responseType.buildRedirectUri(); + try { + responseType.buildRedirectUri(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `redirectUri`'); - } - }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `redirectUri`"); + } + }); - it('should return the new redirect uri and set the `code` and `state` in the query', function() { - const responseType = new CodeResponseType('foo'); - const redirectUri = responseType.buildRedirectUri('http://example.com/cb'); + it("should return the new redirect uri and set the `code` and `state` in the query", function () { + const responseType = new CodeResponseType("foo"); + const redirectUri = responseType.buildRedirectUri( + "http://example.com/cb", + ); - url.format(redirectUri).should.equal('http://example.com/cb?code=foo'); - }); + url.format(redirectUri).should.equal("http://example.com/cb?code=foo"); + }); - it('should return the new redirect uri and append the `code` and `state` in the query', function() { - const responseType = new CodeResponseType('foo'); - const redirectUri = responseType.buildRedirectUri('http://example.com/cb?foo=bar'); + it("should return the new redirect uri and append the `code` and `state` in the query", function () { + const responseType = new CodeResponseType("foo"); + const redirectUri = responseType.buildRedirectUri( + "http://example.com/cb?foo=bar", + ); - url.format(redirectUri).should.equal('http://example.com/cb?foo=bar&code=foo'); - }); - }); + url + .format(redirectUri) + .should.equal("http://example.com/cb?foo=bar&code=foo"); + }); + }); }); diff --git a/test/integration/response_test.js b/test/integration/response_test.js index d6c37e43..baef7d3a 100644 --- a/test/integration/response_test.js +++ b/test/integration/response_test.js @@ -1,75 +1,81 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const Response = require('../../lib/response'); +const Response = require("../../lib/response"); /** * Test `Response` integration. */ -describe('Response integration', function() { - describe('constructor()', function() { - it('should set the `body`', function() { - const response = new Response({ body: 'foo', headers: {} }); +describe("Response integration", function () { + describe("constructor()", function () { + it("should set the `body`", function () { + const response = new Response({ body: "foo", headers: {} }); - response.body.should.equal('foo'); - }); + response.body.should.equal("foo"); + }); - it('should set the `headers`', function() { - const response = new Response({ body: {}, headers: { foo: 'bar', QuX: 'biz' } }); + it("should set the `headers`", function () { + const response = new Response({ + body: {}, + headers: { foo: "bar", QuX: "biz" }, + }); - response.headers.should.eql({ foo: 'bar', qux: 'biz' }); - }); + response.headers.should.eql({ foo: "bar", qux: "biz" }); + }); - it('should set the `status` to 200', function() { - const response = new Response({ body: {}, headers: {} }); + it("should set the `status` to 200", function () { + const response = new Response({ body: {}, headers: {} }); - response.status.should.equal(200); - }); - }); + response.status.should.equal(200); + }); + }); - describe('get()', function() { - it('should return `undefined` if the field does not exist', function() { - const response = new Response({ body: {}, headers: {} }); + describe("get()", function () { + it("should return `undefined` if the field does not exist", function () { + const response = new Response({ body: {}, headers: {} }); - (undefined === response.get('content-type')).should.be.true; - }); + (undefined === response.get("content-type")).should.be.true; + }); - it('should return the value if the field exists', function() { - const response = new Response({ body: {}, headers: { 'content-type': 'text/html; charset=utf-8' } }); + it("should return the value if the field exists", function () { + const response = new Response({ + body: {}, + headers: { "content-type": "text/html; charset=utf-8" }, + }); - response.get('Content-Type').should.equal('text/html; charset=utf-8'); - }); - }); + response.get("Content-Type").should.equal("text/html; charset=utf-8"); + }); + }); - describe('redirect()', function() { - it('should set the location header to `url`', function() { - const response = new Response({ body: {}, headers: {} }); + describe("redirect()", function () { + it("should set the location header to `url`", function () { + const response = new Response({ body: {}, headers: {} }); - response.redirect('http://example.com'); + response.redirect("http://example.com"); - response.get('Location').should.equal('http://example.com'); - }); + response.get("Location").should.equal("http://example.com"); + }); - it('should set the `status` to 302', function() { - const response = new Response({ body: {}, headers: {} }); + it("should set the `status` to 302", function () { + const response = new Response({ body: {}, headers: {} }); - response.redirect('http://example.com'); + response.redirect("http://example.com"); - response.status.should.equal(302); - }); - }); + response.status.should.equal(302); + }); + }); - describe('set()', function() { - it('should set the `field`', function() { - const response = new Response({ body: {}, headers: {} }); + describe("set()", function () { + it("should set the `field`", function () { + const response = new Response({ body: {}, headers: {} }); - response.set('foo', 'bar'); + response.set("foo", "bar"); - response.headers.should.eql({ foo: 'bar' }); - }); - }); + response.headers.should.eql({ foo: "bar" }); + }); + }); }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index f16aecce..ba6cde0f 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -1,183 +1,248 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); -const Model = require('../../lib/model'); -const Request = require('../../lib/request'); -const Response = require('../../lib/response'); -const Server = require('../../lib/server'); -const should = require('chai').should(); +const InvalidArgumentError = require("../../lib/errors/invalid-argument-error"); +const Model = require("../../lib/model"); +const Request = require("../../lib/request"); +const Response = require("../../lib/response"); +const Server = require("../../lib/server"); +const should = require("chai").should(); /** * Test `Server` integration. */ -describe('Server integration', function() { - describe('constructor()', function() { - it('should throw an error if `model` is missing', function() { - [null, undefined, {}].forEach(options => { - try { - new Server(options); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `model`'); - } - }); - }); - - it('should set the `model`', function() { - const model = Model.from({}); - const server = new Server({ model: model }); - - server.options.model.should.equal(model); - }); - }); - - describe('authenticate()', function() { - it('should set the default `options`', async function() { - const model = Model.from({ - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - } - }); - const server = new Server({ model: model }); - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - const response = new Response({ body: {}, headers: {} }); - - try { - await server.authenticate(request, response); - } catch (e) { - server.addAcceptedScopesHeader.should.be.true; - server.addAuthorizedScopesHeader.should.be.true; - server.allowBearerTokensInQueryString.should.be.false; - should.fail(); - } - }); - - it('should return a promise', function() { - const model = Model.from({ - getAccessToken: async function(token) { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - } - }); - const server = new Server({ model: model }); - const request = new Request({ body: {}, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: {} }); - const response = new Response({ body: {}, headers: {} }); - const handler = server.authenticate(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - }); - - describe('authorize()', function() { - it('should set the default `options`', async function() { - const model = Model.from({ - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 123 }; - } - }); - const server = new Server({ model: model }); - const request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); - const response = new Response({ body: {}, headers: {} }); - - try { - await server.authorize(request, response); - } catch (e) { - server.allowEmptyState.should.be.false; - server.authorizationCodeLifetime.should.equal(300); - should.fail(); - } - }); - - it('should return a promise', function() { - const model = Model.from({ - getAccessToken: function() { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }; - }, - getClient: function() { - return { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - }, - saveAuthorizationCode: function() { - return { authorizationCode: 123 }; - } - }); - const server = new Server({ model: model }); - const request = new Request({ body: { client_id: 1234, client_secret: 'secret', response_type: 'code' }, headers: { 'Authorization': 'Bearer foo' }, method: {}, query: { state: 'foobar' } }); - const response = new Response({ body: {}, headers: {} }); - const handler = server.authorize(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - }); - - describe('token()', function() { - it('should set the default `options`', async function() { - const model = Model.from({ - getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { - return { accessToken: 1234, client: {}, user: {} }; - }, - validateScope: function() { return ['foo']; } - }); - const server = new Server({ model: model }); - const request = new Request({ body: { client_id: 1234, client_secret: 'secret', grant_type: 'password', username: 'foo', password: 'pass', scope: 'foo' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); - const response = new Response({ body: {}, headers: {} }); - - try { - await server.token(request, response); - } catch (e) { - server.accessTokenLifetime.should.equal(3600); - server.refreshTokenLifetime.should.equal(1209600); - should.fail(); - } - }); - - it('should return a promise', function() { - const model = Model.from({ - getClient: function() { - return { grants: ['password'] }; - }, - getUser: function() { - return {}; - }, - saveToken: function() { - return { accessToken: 1234, client: {}, user: {} }; - } - }); - const server = new Server({ model: model }); - const request = new Request({ body: { client_id: 1234, client_secret: 'secret', grant_type: 'password', username: 'foo', password: 'pass' }, headers: { 'content-type': 'application/x-www-form-urlencoded', 'transfer-encoding': 'chunked' }, method: 'POST', query: {} }); - const response = new Response({ body: {}, headers: {} }); - const handler = server.token(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - }); +describe("Server integration", function () { + describe("constructor()", function () { + it("should throw an error if `model` is missing", function () { + [null, undefined, {}].forEach((options) => { + try { + new Server(options); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `model`"); + } + }); + }); + + it("should set the `model`", function () { + const model = Model.from({}); + const server = new Server({ model: model }); + + server.options.model.should.equal(model); + }); + }); + + describe("authenticate()", function () { + it("should set the default `options`", async function () { + const model = Model.from({ + getAccessToken: function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await server.authenticate(request, response); + } catch (e) { + server.addAcceptedScopesHeader.should.be.true; + server.addAuthorizedScopesHeader.should.be.true; + server.allowBearerTokensInQueryString.should.be.false; + should.fail(); + } + }); + + it("should return a promise", function () { + const model = Model.from({ + getAccessToken: async function (token) { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + const handler = server.authenticate(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + }); + + describe("authorize()", function () { + it("should set the default `options`", async function () { + const model = Model.from({ + getAccessToken: function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: function () { + return { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + }, + saveAuthorizationCode: function () { + return { authorizationCode: 123 }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: "secret", + response_type: "code", + }, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: { state: "foobar" }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await server.authorize(request, response); + } catch (e) { + server.allowEmptyState.should.be.false; + server.authorizationCodeLifetime.should.equal(300); + should.fail(); + } + }); + + it("should return a promise", function () { + const model = Model.from({ + getAccessToken: function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: function () { + return { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + }, + saveAuthorizationCode: function () { + return { authorizationCode: 123 }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: "secret", + response_type: "code", + }, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: { state: "foobar" }, + }); + const response = new Response({ body: {}, headers: {} }); + const handler = server.authorize(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + }); + + describe("token()", function () { + it("should set the default `options`", async function () { + const model = Model.from({ + getClient: function () { + return { grants: ["password"] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return { accessToken: 1234, client: {}, user: {} }; + }, + validateScope: function () { + return ["foo"]; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: "secret", + grant_type: "password", + username: "foo", + password: "pass", + scope: "foo", + }, + headers: { + "content-type": "application/x-www-form-urlencoded", + "transfer-encoding": "chunked", + }, + method: "POST", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await server.token(request, response); + } catch (e) { + server.accessTokenLifetime.should.equal(3600); + server.refreshTokenLifetime.should.equal(1209600); + should.fail(); + } + }); + + it("should return a promise", function () { + const model = Model.from({ + getClient: function () { + return { grants: ["password"] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return { accessToken: 1234, client: {}, user: {} }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: "secret", + grant_type: "password", + username: "foo", + password: "pass", + }, + headers: { + "content-type": "application/x-www-form-urlencoded", + "transfer-encoding": "chunked", + }, + method: "POST", + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + const handler = server.token(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/token-types/bearer-token-type_test.js b/test/integration/token-types/bearer-token-type_test.js index 47b1daa6..e3a862ba 100644 --- a/test/integration/token-types/bearer-token-type_test.js +++ b/test/integration/token-types/bearer-token-type_test.js @@ -1,93 +1,93 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const BearerTokenType = require('../../../lib/token-types/bearer-token-type'); -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const should = require('chai').should(); +const BearerTokenType = require("../../../lib/token-types/bearer-token-type"); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const should = require("chai").should(); /** * Test `BearerTokenType` integration. */ -describe('BearerTokenType integration', function() { - describe('constructor()', function() { - it('should throw an error if `accessToken` is missing', function() { - try { - new BearerTokenType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessToken`'); - } - }); - - it('should set the `accessToken`', function() { - const responseType = new BearerTokenType('foo', 'bar'); - - responseType.accessToken.should.equal('foo'); - }); - - it('should set the `accessTokenLifetime`', function() { - const responseType = new BearerTokenType('foo', 'bar'); - - responseType.accessTokenLifetime.should.equal('bar'); - }); - - it('should set the `refreshToken`', function() { - const responseType = new BearerTokenType('foo', 'bar', 'biz'); - - responseType.refreshToken.should.equal('biz'); - }); - }); - - describe('valueOf()', function() { - it('should return the value representation', function() { - const responseType = new BearerTokenType('foo', 'bar'); - const value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - expires_in: 'bar', - token_type: 'Bearer' - }); - }); - - it('should not include the `expires_in` if not given', function() { - const responseType = new BearerTokenType('foo'); - const value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - token_type: 'Bearer' - }); - }); - - it('should set `refresh_token` if `refreshToken` is defined', function() { - const responseType = new BearerTokenType('foo', 'bar', 'biz'); - const value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - expires_in: 'bar', - refresh_token: 'biz', - token_type: 'Bearer' - }); - }); - - it('should set `expires_in` if `accessTokenLifetime` is defined', function() { - const responseType = new BearerTokenType('foo', 'bar', 'biz'); - const value = responseType.valueOf(); - - value.should.eql({ - access_token: 'foo', - expires_in: 'bar', - refresh_token: 'biz', - token_type: 'Bearer' - }); - }); - }); +describe("BearerTokenType integration", function () { + describe("constructor()", function () { + it("should throw an error if `accessToken` is missing", function () { + try { + new BearerTokenType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `accessToken`"); + } + }); + + it("should set the `accessToken`", function () { + const responseType = new BearerTokenType("foo", "bar"); + + responseType.accessToken.should.equal("foo"); + }); + + it("should set the `accessTokenLifetime`", function () { + const responseType = new BearerTokenType("foo", "bar"); + + responseType.accessTokenLifetime.should.equal("bar"); + }); + + it("should set the `refreshToken`", function () { + const responseType = new BearerTokenType("foo", "bar", "biz"); + + responseType.refreshToken.should.equal("biz"); + }); + }); + + describe("valueOf()", function () { + it("should return the value representation", function () { + const responseType = new BearerTokenType("foo", "bar"); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: "foo", + expires_in: "bar", + token_type: "Bearer", + }); + }); + + it("should not include the `expires_in` if not given", function () { + const responseType = new BearerTokenType("foo"); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: "foo", + token_type: "Bearer", + }); + }); + + it("should set `refresh_token` if `refreshToken` is defined", function () { + const responseType = new BearerTokenType("foo", "bar", "biz"); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: "foo", + expires_in: "bar", + refresh_token: "biz", + token_type: "Bearer", + }); + }); + + it("should set `expires_in` if `accessTokenLifetime` is defined", function () { + const responseType = new BearerTokenType("foo", "bar", "biz"); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: "foo", + expires_in: "bar", + refresh_token: "biz", + token_type: "Bearer", + }); + }); + }); }); diff --git a/test/integration/utils/token-util_test.js b/test/integration/utils/token-util_test.js index edf9c7e9..69594a8a 100644 --- a/test/integration/utils/token-util_test.js +++ b/test/integration/utils/token-util_test.js @@ -1,20 +1,20 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const TokenUtil = require('../../../lib/utils/token-util'); +const TokenUtil = require("../../../lib/utils/token-util"); /** * Test `TokenUtil` integration. */ -describe('TokenUtil integration', function() { - describe('generateRandomToken()', function() { - it('should return a sha-256 token', async function() { - const token = await TokenUtil.generateRandomToken(); - token.should.be.a.sha256(); - }); - }); +describe("TokenUtil integration", function () { + describe("generateRandomToken()", function () { + it("should return a sha-256 token", async function () { + const token = await TokenUtil.generateRandomToken(); + token.should.be.a.sha256(); + }); + }); }); diff --git a/test/unit/errors/oauth-error_test.js b/test/unit/errors/oauth-error_test.js index 6d68a299..9792a9ae 100644 --- a/test/unit/errors/oauth-error_test.js +++ b/test/unit/errors/oauth-error_test.js @@ -1,58 +1,59 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const { describe, it } = require('mocha'); -const should = require('chai').should(); -const OAuthError = require('../../../lib/errors/oauth-error'); +const { describe, it } = require("mocha"); +const should = require("chai").should(); +const OAuthError = require("../../../lib/errors/oauth-error"); /** * Test `OAuthError`. */ -describe('OAuthError', function() { - describe('constructor()', function() { - it('should get `captureStackTrace`', function() { - - const errorFn = function () { throw new OAuthError('test', {name: 'test_error', foo: 'bar'}); }; - - try { - errorFn(); - - should.fail(); - } catch (e) { - - e.should.be.an.instanceOf(OAuthError); - e.name.should.equal('test_error'); - e.foo.should.equal('bar'); - e.message.should.equal('test'); - e.code.should.equal(500); - e.stack.should.not.be.null; - e.stack.should.not.be.undefined; - e.stack.should.include('oauth-error_test.js'); - e.stack.should.include('19'); //error lineNUmber - } - }); - }); - it('supports undefined properties', function () { - const errorFn = function () { throw new OAuthError('test'); }; - - try { - errorFn(); - - should.fail(); - } catch (e) { - - e.should.be.an.instanceOf(OAuthError); - e.name.should.equal('Error'); - e.message.should.equal('test'); - e.code.should.equal(500); - e.stack.should.not.be.null; - e.stack.should.not.be.undefined; - e.stack.should.include('oauth-error_test.js'); - e.stack.should.include('40'); //error lineNUmber - } - }); +describe("OAuthError", function () { + describe("constructor()", function () { + it("should get `captureStackTrace`", function () { + const errorFn = function () { + throw new OAuthError("test", { name: "test_error", foo: "bar" }); + }; + + try { + errorFn(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(OAuthError); + e.name.should.equal("test_error"); + e.foo.should.equal("bar"); + e.message.should.equal("test"); + e.code.should.equal(500); + e.stack.should.not.be.null; + e.stack.should.not.be.undefined; + e.stack.should.include("oauth-error_test.js"); + e.stack.should.include("19"); //error lineNUmber + } + }); + }); + it("supports undefined properties", function () { + const errorFn = function () { + throw new OAuthError("test"); + }; + + try { + errorFn(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(OAuthError); + e.name.should.equal("Error"); + e.message.should.equal("test"); + e.code.should.equal(500); + e.stack.should.not.be.null; + e.stack.should.not.be.undefined; + e.stack.should.include("oauth-error_test.js"); + e.stack.should.include("40"); //error lineNUmber + } + }); }); diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js index 236574ce..c1ea4d8c 100644 --- a/test/unit/grant-types/abstract-grant-type_test.js +++ b/test/unit/grant-types/abstract-grant-type_test.js @@ -1,48 +1,64 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); -const Model = require('../../../lib/model'); -const sinon = require('sinon'); -const should = require('chai').should(); +const AbstractGrantType = require("../../../lib/grant-types/abstract-grant-type"); +const Model = require("../../../lib/model"); +const sinon = require("sinon"); +const should = require("chai").should(); /** * Test `AbstractGrantType`. */ -describe('AbstractGrantType', function() { - describe('generateAccessToken()', function() { - it('should call `model.generateAccessToken()`', function() { - const model = Model.from({ - generateAccessToken: sinon.stub().returns({ client: {}, expiresAt: new Date(), user: {} }) - }); - const handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); +describe("AbstractGrantType", function () { + describe("generateAccessToken()", function () { + it("should call `model.generateAccessToken()`", function () { + const model = Model.from({ + generateAccessToken: sinon + .stub() + .returns({ client: {}, expiresAt: new Date(), user: {} }), + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 120, + model: model, + }); - return handler.generateAccessToken() - .then(function() { - model.generateAccessToken.callCount.should.equal(1); - model.generateAccessToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .generateAccessToken() + .then(function () { + model.generateAccessToken.callCount.should.equal(1); + model.generateAccessToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe('generateRefreshToken()', function() { - it('should call `model.generateRefreshToken()`', function() { - const model = Model.from({ - generateRefreshToken: sinon.stub().returns({ client: {}, expiresAt: new Date(new Date() / 2), user: {} }) - }); - const handler = new AbstractGrantType({ accessTokenLifetime: 120, model: model }); + describe("generateRefreshToken()", function () { + it("should call `model.generateRefreshToken()`", function () { + const model = Model.from({ + generateRefreshToken: sinon + .stub() + .returns({ + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }), + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 120, + model: model, + }); - return handler.generateRefreshToken() - .then(function() { - model.generateRefreshToken.callCount.should.equal(1); - model.generateRefreshToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .generateRefreshToken() + .then(function () { + model.generateRefreshToken.callCount.should.equal(1); + model.generateRefreshToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index 5650f83b..f752cf87 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -1,149 +1,214 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AuthorizationCodeGrantType = require('../../../lib/grant-types/authorization-code-grant-type'); -const Request = require('../../../lib/request'); -const Model = require('../../../lib/model'); -const sinon = require('sinon'); -const should = require('chai').should(); -const stringUtil = require('../../../lib/utils/string-util'); -const crypto = require('crypto'); +const AuthorizationCodeGrantType = require("../../../lib/grant-types/authorization-code-grant-type"); +const Request = require("../../../lib/request"); +const Model = require("../../../lib/model"); +const sinon = require("sinon"); +const should = require("chai").should(); +const stringUtil = require("../../../lib/utils/string-util"); +const crypto = require("crypto"); /** * Test `AuthorizationCodeGrantType`. */ -describe('AuthorizationCodeGrantType', function() { - describe('getAuthorizationCode()', function() { - it('should call `model.getAuthorizationCode()`', function() { - const model = Model.from({ - getAuthorizationCode: sinon.stub().returns({ authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() * 2), user: {} }), - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }); - const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, query: {} }); - const client = {}; +describe("AuthorizationCodeGrantType", function () { + describe("getAuthorizationCode()", function () { + it("should call `model.getAuthorizationCode()`", function () { + const model = Model.from({ + getAuthorizationCode: sinon + .stub() + .returns({ + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() * 2), + user: {}, + }), + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + const client = {}; - return handler.getAuthorizationCode(request, client) - .then(function() { - model.getAuthorizationCode.callCount.should.equal(1); - model.getAuthorizationCode.firstCall.args.should.have.length(1); - model.getAuthorizationCode.firstCall.args[0].should.equal(12345); - model.getAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .getAuthorizationCode(request, client) + .then(function () { + model.getAuthorizationCode.callCount.should.equal(1); + model.getAuthorizationCode.firstCall.args.should.have.length(1); + model.getAuthorizationCode.firstCall.args[0].should.equal(12345); + model.getAuthorizationCode.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe('revokeAuthorizationCode()', function() { - it('should call `model.revokeAuthorizationCode()`', function() { - const model = Model.from({ - getAuthorizationCode: function() {}, - revokeAuthorizationCode: sinon.stub().returns(true), - saveToken: function() {} - }); - const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); - const authorizationCode = {}; + describe("revokeAuthorizationCode()", function () { + it("should call `model.revokeAuthorizationCode()`", function () { + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: sinon.stub().returns(true), + saveToken: function () {}, + }); + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const authorizationCode = {}; - return handler.revokeAuthorizationCode(authorizationCode) - .then(function() { - model.revokeAuthorizationCode.callCount.should.equal(1); - model.revokeAuthorizationCode.firstCall.args.should.have.length(1); - model.revokeAuthorizationCode.firstCall.args[0].should.equal(authorizationCode); - model.revokeAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .revokeAuthorizationCode(authorizationCode) + .then(function () { + model.revokeAuthorizationCode.callCount.should.equal(1); + model.revokeAuthorizationCode.firstCall.args.should.have.length(1); + model.revokeAuthorizationCode.firstCall.args[0].should.equal( + authorizationCode, + ); + model.revokeAuthorizationCode.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - const client = {}; - const user = {}; - const model = Model.from({ - getAuthorizationCode: function() {}, - revokeAuthorizationCode: function() {}, - saveToken: sinon.stub().returns(true) - }); - const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, model: model }); + describe("saveToken()", function () { + it("should call `model.saveToken()`", function () { + const client = {}; + const user = {}; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model: model, + }); - sinon.stub(handler, 'validateScope').returns(['foobiz']); - sinon.stub(handler, 'generateAccessToken').returns(Promise.resolve('foo')); - sinon.stub(handler, 'generateRefreshToken').returns(Promise.resolve('bar')); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns(Promise.resolve('biz')); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns(Promise.resolve('baz')); + sinon.stub(handler, "validateScope").returns(["foobiz"]); + sinon + .stub(handler, "generateAccessToken") + .returns(Promise.resolve("foo")); + sinon + .stub(handler, "generateRefreshToken") + .returns(Promise.resolve("bar")); + sinon + .stub(handler, "getAccessTokenExpiresAt") + .returns(Promise.resolve("biz")); + sinon + .stub(handler, "getRefreshTokenExpiresAt") + .returns(Promise.resolve("baz")); - return handler.saveToken(user, client, 'foobar', ['foobiz']) - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', authorizationCode: 'foobar', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: ['foobiz'] }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .saveToken(user, client, "foobar", ["foobiz"]) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: "foo", + authorizationCode: "foobar", + accessTokenExpiresAt: "biz", + refreshToken: "bar", + refreshTokenExpiresAt: "baz", + scope: ["foobiz"], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe('with PKCE', function() { - // xxx: the tests for `getAuthorizationCode` are removed, because PKCE is now validated - // in the handle method to ensure token revocation is performed before PKCE validation. + describe("with PKCE", function () { + // xxx: the tests for `getAuthorizationCode` are removed, because PKCE is now validated + // in the handle method to ensure token revocation is performed before PKCE validation. - it('should return an auth code when `code_verifier` is valid with S256 code challenge method', function() { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: 'foobar', isPublic: true }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: 'S256', - codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) - }; - const client = { id: 'foobar', isPublic: true }; - const model = Model.from({ - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345, code_verifier: codeVerifier }, headers: {}, method: {}, query: {} }); + it("should return an auth code when `code_verifier` is valid with S256 code challenge method", function () { + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar", isPublic: true }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: "S256", + codeChallenge: stringUtil.base64URLEncode( + crypto.createHash("sha256").update(codeVerifier).digest(), + ), + }; + const client = { id: "foobar", isPublic: true }; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345, code_verifier: codeVerifier }, + headers: {}, + method: {}, + query: {}, + }); - return grantType.getAuthorizationCode(request, client) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); + return grantType + .getAuthorizationCode(request, client) + .then(function (data) { + data.should.equal(authorizationCode); + }) + .catch(should.fail); + }); - it('should return an auth code when `code_verifier` is valid with plain code challenge method', function() { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: 'foobar' }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: 'plain', - codeChallenge: codeVerifier - }; - const client = { id: 'foobar', isPublic: true }; - const model = Model.from({ - getAuthorizationCode: function() { return authorizationCode; }, - revokeAuthorizationCode: function() {}, - saveToken: function() {} - }); - const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, model: model }); - const request = new Request({ body: { code: 12345, code_verifier: codeVerifier }, headers: {}, method: {}, query: {} }); + it("should return an auth code when `code_verifier` is valid with plain code challenge method", function () { + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); + const authorizationCode = { + authorizationCode: 12345, + client: { id: "foobar" }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: "plain", + codeChallenge: codeVerifier, + }; + const client = { id: "foobar", isPublic: true }; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345, code_verifier: codeVerifier }, + headers: {}, + method: {}, + query: {}, + }); - return grantType.getAuthorizationCode(request, client) - .then(function(data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); - }); + return grantType + .getAuthorizationCode(request, client) + .then(function (data) { + data.should.equal(authorizationCode); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/grant-types/client-credentials-grant-type_test.js b/test/unit/grant-types/client-credentials-grant-type_test.js index 17d386cf..a2ef1563 100644 --- a/test/unit/grant-types/client-credentials-grant-type_test.js +++ b/test/unit/grant-types/client-credentials-grant-type_test.js @@ -1,63 +1,75 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); -const Model = require('../../../lib/model'); -const sinon = require('sinon'); -const should = require('chai').should(); +const ClientCredentialsGrantType = require("../../../lib/grant-types/client-credentials-grant-type"); +const Model = require("../../../lib/model"); +const sinon = require("sinon"); +const should = require("chai").should(); /** * Test `ClientCredentialsGrantType`. */ -describe('ClientCredentialsGrantType', function() { - describe('getUserFromClient()', function() { - it('should call `model.getUserFromClient()`', function() { - const model = Model.from({ - getUserFromClient: sinon.stub().returns(true), - saveToken: function() {} - }); - const handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - const client = {}; - - return handler.getUserFromClient(client) - .then(function() { - model.getUserFromClient.callCount.should.equal(1); - model.getUserFromClient.firstCall.args.should.have.length(1); - model.getUserFromClient.firstCall.args[0].should.equal(client); - model.getUserFromClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - const client = {}; - const user = {}; - const model = Model.from({ - getUserFromClient: function() {}, - saveToken: sinon.stub().returns(true) - }); - const handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, model: model }); - - sinon.stub(handler, 'validateScope').returns(['foobar']); - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - - return handler.saveToken(user, client, ['foobar']) - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', scope: ['foobar'] }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); +describe("ClientCredentialsGrantType", function () { + describe("getUserFromClient()", function () { + it("should call `model.getUserFromClient()`", function () { + const model = Model.from({ + getUserFromClient: sinon.stub().returns(true), + saveToken: function () {}, + }); + const handler = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const client = {}; + + return handler + .getUserFromClient(client) + .then(function () { + model.getUserFromClient.callCount.should.equal(1); + model.getUserFromClient.firstCall.args.should.have.length(1); + model.getUserFromClient.firstCall.args[0].should.equal(client); + model.getUserFromClient.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); + + describe("saveToken()", function () { + it("should call `model.saveToken()`", function () { + const client = {}; + const user = {}; + const model = Model.from({ + getUserFromClient: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + + sinon.stub(handler, "validateScope").returns(["foobar"]); + sinon.stub(handler, "generateAccessToken").returns("foo"); + sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); + + return handler + .saveToken(user, client, ["foobar"]) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: "foo", + accessTokenExpiresAt: "biz", + scope: ["foobar"], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/grant-types/password-grant-type_test.js b/test/unit/grant-types/password-grant-type_test.js index 398b2929..7d79ca75 100644 --- a/test/unit/grant-types/password-grant-type_test.js +++ b/test/unit/grant-types/password-grant-type_test.js @@ -1,68 +1,87 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); -const Request = require('../../../lib/request'); -const Model = require('../../../lib/model'); -const sinon = require('sinon'); -const should = require('chai').should(); +const PasswordGrantType = require("../../../lib/grant-types/password-grant-type"); +const Request = require("../../../lib/request"); +const Model = require("../../../lib/model"); +const sinon = require("sinon"); +const should = require("chai").should(); /** * Test `PasswordGrantType`. */ -describe('PasswordGrantType', function() { - describe('getUser()', function() { - it('should call `model.getUser()`', function() { - const model = Model.from({ - getUser: sinon.stub().returns(true), - saveToken: function() {} - }); - const client = { id: 'foobar' }; - const handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, query: {} }); +describe("PasswordGrantType", function () { + describe("getUser()", function () { + it("should call `model.getUser()`", function () { + const model = Model.from({ + getUser: sinon.stub().returns(true), + saveToken: function () {}, + }); + const client = { id: "foobar" }; + const handler = new PasswordGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { username: "foo", password: "bar" }, + headers: {}, + method: {}, + query: {}, + }); - return handler.getUser(request, client) - .then(function() { - model.getUser.callCount.should.equal(1); - model.getUser.firstCall.args.should.have.length(3); - model.getUser.firstCall.args[0].should.equal('foo'); - model.getUser.firstCall.args[1].should.equal('bar'); - model.getUser.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .getUser(request, client) + .then(function () { + model.getUser.callCount.should.equal(1); + model.getUser.firstCall.args.should.have.length(3); + model.getUser.firstCall.args[0].should.equal("foo"); + model.getUser.firstCall.args[1].should.equal("bar"); + model.getUser.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - const client = {}; - const user = {}; - const model = Model.from({ - getUser: function() {}, - saveToken: sinon.stub().returns(true) - }); - const handler = new PasswordGrantType({ accessTokenLifetime: 120, model: model }); + describe("saveToken()", function () { + it("should call `model.saveToken()`", function () { + const client = {}; + const user = {}; + const model = Model.from({ + getUser: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new PasswordGrantType({ + accessTokenLifetime: 120, + model: model, + }); - sinon.stub(handler, 'validateScope').returns(['foobar']); - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); + sinon.stub(handler, "validateScope").returns(["foobar"]); + sinon.stub(handler, "generateAccessToken").returns("foo"); + sinon.stub(handler, "generateRefreshToken").returns("bar"); + sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); + sinon.stub(handler, "getRefreshTokenExpiresAt").returns("baz"); - return handler.saveToken(user, client, ['foobar']) - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: ['foobar'] }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .saveToken(user, client, ["foobar"]) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: "foo", + accessTokenExpiresAt: "biz", + refreshToken: "bar", + refreshTokenExpiresAt: "baz", + scope: ["foobar"], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js index ed0c9e2a..9d919a43 100644 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ b/test/unit/grant-types/refresh-token-grant-type_test.js @@ -1,201 +1,297 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); -const Request = require('../../../lib/request'); -const Model = require('../../../lib/model'); -const sinon = require('sinon'); -const should = require('chai').should(); +const RefreshTokenGrantType = require("../../../lib/grant-types/refresh-token-grant-type"); +const Request = require("../../../lib/request"); +const Model = require("../../../lib/model"); +const sinon = require("sinon"); +const should = require("chai").should(); /** * Test `RefreshTokenGrantType`. */ -describe('RefreshTokenGrantType', function() { - describe('handle()', function() { - it('should revoke the previous token', function() { - const token = { accessToken: 'foo', client: {}, user: {} }; - const model = Model.from({ - getRefreshToken: function() { return token; }, - saveToken: function() { return { accessToken: 'bar', client: {}, user: {} }; }, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }) - }); - const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, query: {} }); - const client = {}; - - return handler.handle(request, client) - .then(function() { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('getRefreshToken()', function() { - it('should call `model.getRefreshToken()`', function() { - const model = Model.from({ - getRefreshToken: sinon.stub().returns({ accessToken: 'foo', client: {}, user: {} }), - saveToken: function() {}, - revokeToken: function() {} - }); - const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - const request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, query: {} }); - const client = {}; - - return handler.getRefreshToken(request, client) - .then(function() { - model.getRefreshToken.callCount.should.equal(1); - model.getRefreshToken.firstCall.args.should.have.length(1); - model.getRefreshToken.firstCall.args[0].should.equal('bar'); - model.getRefreshToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('revokeToken()', function() { - it('should call `model.revokeToken()`', function() { - const model = Model.from({ - getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), - saveToken: function() {} - }); - const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - const token = {}; - - return handler.revokeToken(token) - .then(function() { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should not call `model.revokeToken()`', function() { - const model = Model.from({ - getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), - saveToken: function() {} - }); - const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: false }); - const token = {}; - - return handler.revokeToken(token) - .then(function() { - model.revokeToken.callCount.should.equal(0); - }) - .catch(should.fail); - }); - - it('should not call `model.revokeToken()`', function() { - const model = Model.from({ - getRefreshToken: function() {}, - revokeToken: sinon.stub().returns({ accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), user: {} }), - saveToken: function() {} - }); - const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: true }); - const token = {}; - - return handler.revokeToken(token) - .then(function() { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('saveToken()', function() { - it('should call `model.saveToken()`', function() { - const client = {}; - const user = {}; - const model = Model.from({ - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: sinon.stub().returns(true) - }); - const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model }); - - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - - return handler.saveToken(user, client, ['foobar']) - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: ['foobar'] }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should call `model.saveToken()` without refresh token', function() { - const client = {}; - const user = {}; - const model = Model.from({ - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: sinon.stub().returns(true) - }); - const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: false }); - - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - - return handler.saveToken(user, client, ['foobar']) - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', scope: ['foobar'] }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should call `model.saveToken()` with refresh token', function() { - const client = {}; - const user = {}; - const model = Model.from({ - getRefreshToken: function() {}, - revokeToken: function() {}, - saveToken: sinon.stub().returns(true) - }); - const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, alwaysIssueNewRefreshToken: true}); - - sinon.stub(handler, 'generateAccessToken').returns('foo'); - sinon.stub(handler, 'generateRefreshToken').returns('bar'); - sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - - return handler.saveToken(user, client, ['foobar']) - .then(function() { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', scope: ['foobar'] }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); +describe("RefreshTokenGrantType", function () { + describe("handle()", function () { + it("should revoke the previous token", function () { + const token = { accessToken: "foo", client: {}, user: {} }; + const model = Model.from({ + getRefreshToken: function () { + return token; + }, + saveToken: function () { + return { accessToken: "bar", client: {}, user: {} }; + }, + revokeToken: sinon + .stub() + .returns({ + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { refresh_token: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + const client = {}; + + return handler + .handle(request, client) + .then(function () { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); + + describe("getRefreshToken()", function () { + it("should call `model.getRefreshToken()`", function () { + const model = Model.from({ + getRefreshToken: sinon + .stub() + .returns({ accessToken: "foo", client: {}, user: {} }), + saveToken: function () {}, + revokeToken: function () {}, + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { refresh_token: "bar" }, + headers: {}, + method: {}, + query: {}, + }); + const client = {}; + + return handler + .getRefreshToken(request, client) + .then(function () { + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args.should.have.length(1); + model.getRefreshToken.firstCall.args[0].should.equal("bar"); + model.getRefreshToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); + + describe("revokeToken()", function () { + it("should call `model.revokeToken()`", function () { + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: sinon + .stub() + .returns({ + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), + saveToken: function () {}, + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const token = {}; + + return handler + .revokeToken(token) + .then(function () { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + + it("should not call `model.revokeToken()`", function () { + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: sinon + .stub() + .returns({ + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), + saveToken: function () {}, + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + alwaysIssueNewRefreshToken: false, + }); + const token = {}; + + return handler + .revokeToken(token) + .then(function () { + model.revokeToken.callCount.should.equal(0); + }) + .catch(should.fail); + }); + + it("should not call `model.revokeToken()`", function () { + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: sinon + .stub() + .returns({ + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), + saveToken: function () {}, + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + alwaysIssueNewRefreshToken: true, + }); + const token = {}; + + return handler + .revokeToken(token) + .then(function () { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); + + describe("saveToken()", function () { + it("should call `model.saveToken()`", function () { + const client = {}; + const user = {}; + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + }); + + sinon.stub(handler, "generateAccessToken").returns("foo"); + sinon.stub(handler, "generateRefreshToken").returns("bar"); + sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); + sinon.stub(handler, "getRefreshTokenExpiresAt").returns("baz"); + + return handler + .saveToken(user, client, ["foobar"]) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: "foo", + accessTokenExpiresAt: "biz", + refreshToken: "bar", + refreshTokenExpiresAt: "baz", + scope: ["foobar"], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + + it("should call `model.saveToken()` without refresh token", function () { + const client = {}; + const user = {}; + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + alwaysIssueNewRefreshToken: false, + }); + + sinon.stub(handler, "generateAccessToken").returns("foo"); + sinon.stub(handler, "generateRefreshToken").returns("bar"); + sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); + sinon.stub(handler, "getRefreshTokenExpiresAt").returns("baz"); + + return handler + .saveToken(user, client, ["foobar"]) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: "foo", + accessTokenExpiresAt: "biz", + scope: ["foobar"], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + + it("should call `model.saveToken()` with refresh token", function () { + const client = {}; + const user = {}; + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + alwaysIssueNewRefreshToken: true, + }); + + sinon.stub(handler, "generateAccessToken").returns("foo"); + sinon.stub(handler, "generateRefreshToken").returns("bar"); + sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); + sinon.stub(handler, "getRefreshTokenExpiresAt").returns("baz"); + + return handler + .saveToken(user, client, ["foobar"]) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: "foo", + accessTokenExpiresAt: "biz", + refreshToken: "bar", + refreshTokenExpiresAt: "baz", + scope: ["foobar"], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index e5384e01..72a93d37 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -1,182 +1,197 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); -const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); -const Request = require('../../../lib/request'); -const Model = require('../../../lib/model'); -const sinon = require('sinon'); -const should = require('chai').should(); -const ServerError = require('../../../lib/errors/server-error'); +const AuthenticateHandler = require("../../../lib/handlers/authenticate-handler"); +const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); +const Request = require("../../../lib/request"); +const Model = require("../../../lib/model"); +const sinon = require("sinon"); +const should = require("chai").should(); +const ServerError = require("../../../lib/errors/server-error"); /** * Test `AuthenticateHandler`. */ -describe('AuthenticateHandler', function() { - describe('getTokenFromRequest()', function() { - describe('with bearer token in the request authorization header', function() { - it('should throw an error if the token is malformed', () => { - const handler = new AuthenticateHandler({ - model: { getAccessToken() {} }, - }); - const request = new Request({ - body: {}, - headers: { - Authorization: 'foo Bearer bar', - }, - method: 'ANY', - query: {}, - }); - - try { - handler.getTokenFromRequestHeader(request); - - should.fail('should.fail', ''); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: malformed authorization header', - ); - } - }); - }); - - describe('with bearer token in the request authorization header', function() { - it('should call `getTokenFromRequestHeader()`', function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ - body: {}, - headers: { 'Authorization': 'Bearer foo' }, - method: {}, - query: {} - }); - - sinon.stub(handler, 'getTokenFromRequestHeader'); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestHeader.callCount.should.equal(1); - handler.getTokenFromRequestHeader.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestHeader.restore(); - }); - }); - - describe('with bearer token in the request query', function() { - it('should call `getTokenFromRequestQuery()`', function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: { access_token: 'foo' } - }); - - sinon.stub(handler, 'getTokenFromRequestQuery'); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestQuery.callCount.should.equal(1); - handler.getTokenFromRequestQuery.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestQuery.restore(); - }); - }); - - describe('with bearer token in the request body', function() { - it('should call `getTokenFromRequestBody()`', function() { - const handler = new AuthenticateHandler({ model: { getAccessToken: function() {} } }); - const request = new Request({ - body: { access_token: 'foo' }, - headers: {}, - method: {}, - query: {} - }); - - sinon.stub(handler, 'getTokenFromRequestBody'); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestBody.callCount.should.equal(1); - handler.getTokenFromRequestBody.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestBody.restore(); - }); - }); - }); - - describe('getAccessToken()', function() { - it('should call `model.getAccessToken()`', function() { - const model = Model.from({ - getAccessToken: sinon.stub().returns({ user: {} }) - }); - const handler = new AuthenticateHandler({ model: model }); - - return handler.getAccessToken('foo') - .then(function() { - model.getAccessToken.callCount.should.equal(1); - model.getAccessToken.firstCall.args.should.have.length(1); - model.getAccessToken.firstCall.args[0].should.equal('foo'); - model.getAccessToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('validateAccessToken()', function() { - it('should fail if token has no valid `accessTokenExpiresAt` date', function() { - const model = Model.from({ - getAccessToken: function() {} - }); - const handler = new AuthenticateHandler({ model: model }); - - let failed = false; - try { - handler.validateAccessToken({ - user: {} - }); - } - catch (err) { - err.should.be.an.instanceOf(ServerError); - failed = true; - } - failed.should.equal(true); - }); - - it('should succeed if token has valid `accessTokenExpiresAt` date', function() { - const model = Model.from({ - getAccessToken: function() {} - }); - const handler = new AuthenticateHandler({ model: model }); - try { - handler.validateAccessToken({ - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000) - }); - } - catch (err) { - should.fail(); - } - }); - }); - - describe('verifyScope()', function() { - it('should call `model.getAccessToken()` if scope is defined', function() { - const model = Model.from({ - getAccessToken: function() {}, - verifyScope: sinon.stub().returns(true) - }); - const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, scope: 'bar' }); - - return handler.verifyScope(['foo']) - .then(function() { - model.verifyScope.callCount.should.equal(1); - model.verifyScope.firstCall.args.should.have.length(2); - model.verifyScope.firstCall.args[0].should.eql(['foo'], ['bar']); - model.verifyScope.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); +describe("AuthenticateHandler", function () { + describe("getTokenFromRequest()", function () { + describe("with bearer token in the request authorization header", function () { + it("should throw an error if the token is malformed", () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: "foo Bearer bar", + }, + method: "ANY", + query: {}, + }); + + try { + handler.getTokenFromRequestHeader(request); + + should.fail("should.fail", ""); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: malformed authorization header", + ); + } + }); + }); + + describe("with bearer token in the request authorization header", function () { + it("should call `getTokenFromRequestHeader()`", function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: { Authorization: "Bearer foo" }, + method: {}, + query: {}, + }); + + sinon.stub(handler, "getTokenFromRequestHeader"); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestHeader.callCount.should.equal(1); + handler.getTokenFromRequestHeader.firstCall.args[0].should.equal( + request, + ); + handler.getTokenFromRequestHeader.restore(); + }); + }); + + describe("with bearer token in the request query", function () { + it("should call `getTokenFromRequestQuery()`", function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { access_token: "foo" }, + }); + + sinon.stub(handler, "getTokenFromRequestQuery"); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestQuery.callCount.should.equal(1); + handler.getTokenFromRequestQuery.firstCall.args[0].should.equal( + request, + ); + handler.getTokenFromRequestQuery.restore(); + }); + }); + + describe("with bearer token in the request body", function () { + it("should call `getTokenFromRequestBody()`", function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: { access_token: "foo" }, + headers: {}, + method: {}, + query: {}, + }); + + sinon.stub(handler, "getTokenFromRequestBody"); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestBody.callCount.should.equal(1); + handler.getTokenFromRequestBody.firstCall.args[0].should.equal(request); + handler.getTokenFromRequestBody.restore(); + }); + }); + }); + + describe("getAccessToken()", function () { + it("should call `model.getAccessToken()`", function () { + const model = Model.from({ + getAccessToken: sinon.stub().returns({ user: {} }), + }); + const handler = new AuthenticateHandler({ model: model }); + + return handler + .getAccessToken("foo") + .then(function () { + model.getAccessToken.callCount.should.equal(1); + model.getAccessToken.firstCall.args.should.have.length(1); + model.getAccessToken.firstCall.args[0].should.equal("foo"); + model.getAccessToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); + + describe("validateAccessToken()", function () { + it("should fail if token has no valid `accessTokenExpiresAt` date", function () { + const model = Model.from({ + getAccessToken: function () {}, + }); + const handler = new AuthenticateHandler({ model: model }); + + let failed = false; + try { + handler.validateAccessToken({ + user: {}, + }); + } catch (err) { + err.should.be.an.instanceOf(ServerError); + failed = true; + } + failed.should.equal(true); + }); + + it("should succeed if token has valid `accessTokenExpiresAt` date", function () { + const model = Model.from({ + getAccessToken: function () {}, + }); + const handler = new AuthenticateHandler({ model: model }); + try { + handler.validateAccessToken({ + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }); + } catch (err) { + should.fail(); + } + }); + }); + + describe("verifyScope()", function () { + it("should call `model.getAccessToken()` if scope is defined", function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: sinon.stub().returns(true), + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: "bar", + }); + + return handler + .verifyScope(["foo"]) + .then(function () { + model.verifyScope.callCount.should.equal(1); + model.verifyScope.firstCall.args.should.have.length(2); + model.verifyScope.firstCall.args[0].should.eql(["foo"], ["bar"]); + model.verifyScope.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index b539e480..0d1046ad 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -1,198 +1,297 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); -const Request = require('../../../lib/request'); -const Response = require('../../../lib/response'); -const Model = require('../../../lib/model'); -const sinon = require('sinon'); -const should = require('chai').should(); +const AuthorizeHandler = require("../../../lib/handlers/authorize-handler"); +const Request = require("../../../lib/request"); +const Response = require("../../../lib/response"); +const Model = require("../../../lib/model"); +const sinon = require("sinon"); +const should = require("chai").should(); /** * Test `AuthorizeHandler`. */ -describe('AuthorizeHandler', function() { - describe('generateAuthorizationCode()', function() { - it('should call `model.generateAuthorizationCode()`', function() { - const model = Model.from({ - generateAuthorizationCode: sinon.stub().returns({}), - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - return handler.generateAuthorizationCode() - .then(function() { - model.generateAuthorizationCode.callCount.should.equal(1); - model.generateAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('getClient()', function() { - it('should call `model.getClient()`', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: sinon.stub().returns({ grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }), - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function() { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('getUser()', function() { - it('should call `authenticateHandler.getUser()`', function() { - const authenticateHandler = { handle: sinon.stub().returns(Promise.resolve({})) }; - const model = Model.from({ - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, model: model }); - const request = new Request({ body: {}, headers: {}, method: {}, query: {} }); - const response = new Response(); - - return handler.getUser(request, response) - .then(function() { - authenticateHandler.handle.callCount.should.equal(1); - authenticateHandler.handle.firstCall.args.should.have.length(2); - authenticateHandler.handle.firstCall.args[0].should.equal(request); - authenticateHandler.handle.firstCall.args[1].should.equal(response); - }) - .catch(should.fail); - }); - }); - - describe('saveAuthorizationCode()', function() { - it('should call `model.saveAuthorizationCode()`', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: sinon.stub().returns({}) - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - return handler.saveAuthorizationCode('foo', 'bar', ['qux'], 'biz', 'baz', 'boz') - .then(function() { - model.saveAuthorizationCode.callCount.should.equal(1); - model.saveAuthorizationCode.firstCall.args.should.have.length(3); - model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: ['qux'] }); - model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); - model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); - model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should call `model.saveAuthorizationCode()` with code challenge', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: sinon.stub().returns({}) - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - - return handler.saveAuthorizationCode('foo', 'bar', ['qux'], 'biz', 'baz', 'boz', 'codeChallenge', 'codeChallengeMethod') - .then(function() { - model.saveAuthorizationCode.callCount.should.equal(1); - model.saveAuthorizationCode.firstCall.args.should.have.length(3); - model.saveAuthorizationCode.firstCall.args[0].should.eql({ authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', scope: ['qux'], codeChallenge: 'codeChallenge', codeChallengeMethod: 'codeChallengeMethod' }); - model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); - model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); - model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe('validateRedirectUri()', function() { - it('should call `model.validateRedirectUri()`', function() { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const redirect_uri = 'http://example.com/cb/2'; - const model = Model.from({ - getAccessToken: function() {}, - getClient: sinon.stub().returns(client), - saveAuthorizationCode: function() {}, - validateRedirectUri: sinon.stub().returns(true) - }); - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(function() { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.thisValue.should.equal(model); - - model.validateRedirectUri.callCount.should.equal(1); - model.validateRedirectUri.firstCall.args.should.have.length(2); - model.validateRedirectUri.firstCall.args[0].should.equal(redirect_uri); - model.validateRedirectUri.firstCall.args[1].should.equal(client); - model.validateRedirectUri.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - - it('should be successful validation', function () { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const redirect_uri = 'http://example.com/cb'; - const model = Model.from({ - getAccessToken: function() {}, - getClient: sinon.stub().returns(client), - saveAuthorizationCode: function() {}, - validateRedirectUri: function (redirectUri, client) { - return client.redirectUris.includes(redirectUri); - } - }); - - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then((client) => { - client.should.equal(client); - }); - }); - - it('should be unsuccessful validation', function () { - const client = { grants: ['authorization_code'], redirectUris: ['http://example.com/cb'] }; - const redirect_uri = 'http://example.com/callback'; - const model = Model.from({ - getAccessToken: function() {}, - getClient: sinon.stub().returns(client), - saveAuthorizationCode: function() {}, - validateRedirectUri: function (redirectUri, client) { - return client.redirectUris.includes(redirectUri); - } - }); - - const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model: model }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, query: {} }); - - return handler.getClient(request) - .then(() => { - throw Error('should not resolve'); - }) - .catch((err) => { - err.name.should.equal('invalid_client'); - err.message.should.equal('Invalid client: `redirect_uri` does not match client value'); - }); - }); - }); +describe("AuthorizeHandler", function () { + describe("generateAuthorizationCode()", function () { + it("should call `model.generateAuthorizationCode()`", function () { + const model = Model.from({ + generateAuthorizationCode: sinon.stub().returns({}), + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + + return handler + .generateAuthorizationCode() + .then(function () { + model.generateAuthorizationCode.callCount.should.equal(1); + model.generateAuthorizationCode.firstCall.thisValue.should.equal( + model, + ); + }) + .catch(should.fail); + }); + }); + + describe("getClient()", function () { + it("should call `model.getClient()`", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: sinon + .stub() + .returns({ + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }), + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret" }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(function () { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); + + describe("getUser()", function () { + it("should call `authenticateHandler.getUser()`", function () { + const authenticateHandler = { + handle: sinon.stub().returns(Promise.resolve({})), + }; + const model = Model.from({ + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authenticateHandler: authenticateHandler, + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + const response = new Response(); + + return handler + .getUser(request, response) + .then(function () { + authenticateHandler.handle.callCount.should.equal(1); + authenticateHandler.handle.firstCall.args.should.have.length(2); + authenticateHandler.handle.firstCall.args[0].should.equal(request); + authenticateHandler.handle.firstCall.args[1].should.equal(response); + }) + .catch(should.fail); + }); + }); + + describe("saveAuthorizationCode()", function () { + it("should call `model.saveAuthorizationCode()`", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: sinon.stub().returns({}), + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + + return handler + .saveAuthorizationCode("foo", "bar", ["qux"], "biz", "baz", "boz") + .then(function () { + model.saveAuthorizationCode.callCount.should.equal(1); + model.saveAuthorizationCode.firstCall.args.should.have.length(3); + model.saveAuthorizationCode.firstCall.args[0].should.eql({ + authorizationCode: "foo", + expiresAt: "bar", + redirectUri: "baz", + scope: ["qux"], + }); + model.saveAuthorizationCode.firstCall.args[1].should.equal("biz"); + model.saveAuthorizationCode.firstCall.args[2].should.equal("boz"); + model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + + it("should call `model.saveAuthorizationCode()` with code challenge", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: sinon.stub().returns({}), + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + + return handler + .saveAuthorizationCode( + "foo", + "bar", + ["qux"], + "biz", + "baz", + "boz", + "codeChallenge", + "codeChallengeMethod", + ) + .then(function () { + model.saveAuthorizationCode.callCount.should.equal(1); + model.saveAuthorizationCode.firstCall.args.should.have.length(3); + model.saveAuthorizationCode.firstCall.args[0].should.eql({ + authorizationCode: "foo", + expiresAt: "bar", + redirectUri: "baz", + scope: ["qux"], + codeChallenge: "codeChallenge", + codeChallengeMethod: "codeChallengeMethod", + }); + model.saveAuthorizationCode.firstCall.args[1].should.equal("biz"); + model.saveAuthorizationCode.firstCall.args[2].should.equal("boz"); + model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); + + describe("validateRedirectUri()", function () { + it("should call `model.validateRedirectUri()`", function () { + const client = { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const redirect_uri = "http://example.com/cb/2"; + const model = Model.from({ + getAccessToken: function () {}, + getClient: sinon.stub().returns(client), + saveAuthorizationCode: function () {}, + validateRedirectUri: sinon.stub().returns(true), + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret", redirect_uri }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(function () { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.thisValue.should.equal(model); + + model.validateRedirectUri.callCount.should.equal(1); + model.validateRedirectUri.firstCall.args.should.have.length(2); + model.validateRedirectUri.firstCall.args[0].should.equal( + redirect_uri, + ); + model.validateRedirectUri.firstCall.args[1].should.equal(client); + model.validateRedirectUri.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + + it("should be successful validation", function () { + const client = { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const redirect_uri = "http://example.com/cb"; + const model = Model.from({ + getAccessToken: function () {}, + getClient: sinon.stub().returns(client), + saveAuthorizationCode: function () {}, + validateRedirectUri: function (redirectUri, client) { + return client.redirectUris.includes(redirectUri); + }, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret", redirect_uri }, + headers: {}, + method: {}, + query: {}, + }); + + return handler.getClient(request).then((client) => { + client.should.equal(client); + }); + }); + + it("should be unsuccessful validation", function () { + const client = { + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }; + const redirect_uri = "http://example.com/callback"; + const model = Model.from({ + getAccessToken: function () {}, + getClient: sinon.stub().returns(client), + saveAuthorizationCode: function () {}, + validateRedirectUri: function (redirectUri, client) { + return client.redirectUris.includes(redirectUri); + }, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret", redirect_uri }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(() => { + throw Error("should not resolve"); + }) + .catch((err) => { + err.name.should.equal("invalid_client"); + err.message.should.equal( + "Invalid client: `redirect_uri` does not match client value", + ); + }); + }); + }); }); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js index a16be32d..269ebc13 100644 --- a/test/unit/handlers/token-handler_test.js +++ b/test/unit/handlers/token-handler_test.js @@ -1,38 +1,48 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const Request = require('../../../lib/request'); -const Model = require('../../../lib/model'); -const TokenHandler = require('../../../lib/handlers/token-handler'); -const sinon = require('sinon'); -const should = require('chai').should(); +const Request = require("../../../lib/request"); +const Model = require("../../../lib/model"); +const TokenHandler = require("../../../lib/handlers/token-handler"); +const sinon = require("sinon"); +const should = require("chai").should(); /** * Test `TokenHandler`. */ -describe('TokenHandler', function() { - describe('getClient()', function() { - it('should call `model.getClient()`', function() { - const model = Model.from({ - getClient: sinon.stub().returns({ grants: ['password'] }), - saveToken: function() {} - }); - const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 }); - const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, query: {} }); +describe("TokenHandler", function () { + describe("getClient()", function () { + it("should call `model.getClient()`", function () { + const model = Model.from({ + getClient: sinon.stub().returns({ grants: ["password"] }), + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: "secret" }, + headers: {}, + method: {}, + query: {}, + }); - return handler.getClient(request) - .then(function() { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.args[1].should.equal('secret'); - model.getClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .getClient(request) + .then(function () { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.args[1].should.equal("secret"); + model.getClient.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/models/model_wrapper_test.js b/test/unit/models/model_wrapper_test.js index 75b8ebb4..ddca59dd 100644 --- a/test/unit/models/model_wrapper_test.js +++ b/test/unit/models/model_wrapper_test.js @@ -1,33 +1,32 @@ -const Model = require('../../../lib/model'); -const {expect} = require('chai'); +const Model = require("../../../lib/model"); +const { expect } = require("chai"); -describe('ModelWrapper', () => { - const expectThrows = async (fn) => { - try { - await fn(); - expect.fail(); - } catch (e) { - expect(e.message).to.include('not implemented'); - } - }; - it('throws on all functions when used via constructor', async () => { - const m = new Model(); - await expectThrows(() => m.generateAccessToken()); - await expectThrows(() => m.generateAuthorizationCode()); - await expectThrows(() => m.generateRefreshToken()); - await expectThrows(() => m.getAccessToken()); - await expectThrows(() => m.getAuthorizationCode()); - await expectThrows(() => m.getClient()); - await expectThrows(() => m.getRefreshToken()); - await expectThrows(() => m.getUser()); - await expectThrows(() => m.getUserFromClient()); - await expectThrows(() => m.revokeAuthorizationCode()); - await expectThrows(() => m.revokeToken()); - await expectThrows(() => m.saveAuthorizationCode()); - await expectThrows(() => m.saveToken()); - await expectThrows(() => m.verifyScope()); - await expectThrows(() => m.validateRedirectUri()); - await expectThrows(() => m.validateScope()); - - }); -}); \ No newline at end of file +describe("ModelWrapper", () => { + const expectThrows = async (fn) => { + try { + await fn(); + expect.fail(); + } catch (e) { + expect(e.message).to.include("not implemented"); + } + }; + it("throws on all functions when used via constructor", async () => { + const m = new Model(); + await expectThrows(() => m.generateAccessToken()); + await expectThrows(() => m.generateAuthorizationCode()); + await expectThrows(() => m.generateRefreshToken()); + await expectThrows(() => m.getAccessToken()); + await expectThrows(() => m.getAuthorizationCode()); + await expectThrows(() => m.getClient()); + await expectThrows(() => m.getRefreshToken()); + await expectThrows(() => m.getUser()); + await expectThrows(() => m.getUserFromClient()); + await expectThrows(() => m.revokeAuthorizationCode()); + await expectThrows(() => m.revokeToken()); + await expectThrows(() => m.saveAuthorizationCode()); + await expectThrows(() => m.saveToken()); + await expectThrows(() => m.verifyScope()); + await expectThrows(() => m.validateRedirectUri()); + await expectThrows(() => m.validateScope()); + }); +}); diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model_test.js index 338a7f9d..6eed7f8f 100644 --- a/test/unit/models/token-model_test.js +++ b/test/unit/models/token-model_test.js @@ -1,155 +1,160 @@ -const TokenModel = require('../../../lib/models/token-model'); -const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); -const should = require('chai').should(); +const TokenModel = require("../../../lib/models/token-model"); +const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); +const should = require("chai").should(); /** * Test `Server`. */ -describe('TokenModel', function() { - describe('constructor()', function() { - it('throws, if data is empty', function () { - try { - new TokenModel(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessToken`'); - } - }); - it('throws, if `accessToken` is missing', function () { - const atExpiresAt = new Date(); - atExpiresAt.setHours(new Date().getHours() + 1); - - const data = { - client: 'bar', - user: 'tar', - accessTokenExpiresAt: atExpiresAt - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `accessToken`'); - } - }); - it('throws, if `client` is missing', function () { - const atExpiresAt = new Date(); - atExpiresAt.setHours(new Date().getHours() + 1); - - const data = { - accessToken: 'foo', - user: 'tar', - accessTokenExpiresAt: atExpiresAt - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `client`'); - } - }); - it('throws, if `user` is missing', function () { - const atExpiresAt = new Date(); - atExpiresAt.setHours(new Date().getHours() + 1); - - const data = { - accessToken: 'foo', - client: 'bar', - accessTokenExpiresAt: atExpiresAt - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Missing parameter: `user`'); - } - }); - it('throws, if `accessTokenExpiresAt` is not a Date', function () { - const data = { - accessToken: 'foo', - client: 'bar', - user: 'tar', - accessTokenExpiresAt: '11/10/2023' - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid parameter: `accessTokenExpiresAt`'); - } - }); - it('throws, if `refreshTokenExpiresAt` is not a Date', function () { - const data = { - accessToken: 'foo', - client: 'bar', - user: 'tar', - refreshTokenExpiresAt: '11/10/2023' - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal('Invalid parameter: `refreshTokenExpiresAt`'); - } - }); - it('should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set', function() { - const atExpiresAt = new Date(); - atExpiresAt.setHours(new Date().getHours() + 1); - - const data = { - accessToken: 'foo', - client: 'bar', - user: 'tar', - accessTokenExpiresAt: atExpiresAt - }; - - const model = new TokenModel(data); - should.exist(model.accessTokenLifetime); - model.accessTokenLifetime.should.a('number'); - model.accessTokenLifetime.should.be.approximately(3600, 2); - }); - - it('should throw if the required arguments are not provided', () => { - should.throw(() => { - new TokenModel({}); - }); - }); - - it('should ignore custom attributes if allowExtendedTokenAttributes is not specified as true', () => { - const model = new TokenModel({ - accessToken: 'token', - client: 'client', - user: 'user', - myCustomAttribute: 'myCustomValue' - }); - - should.not.exist(model['myCustomAttribute']); - should.not.exist(model['customAttributes']); - }); - - it('should set custom attributes on the customAttributes field if allowExtendedTokenAttributes is specified as true', () => { - const model = new TokenModel({ - accessToken: 'token', - client: 'client', - user: 'user', - myCustomAttribute: 'myCustomValue' - }, { - allowExtendedTokenAttributes: true - }); - - should.not.exist(model['myCustomAttribute']); - model['customAttributes'].should.be.an('object'); - model['customAttributes']['myCustomAttribute'].should.equal('myCustomValue'); - }); - }); +describe("TokenModel", function () { + describe("constructor()", function () { + it("throws, if data is empty", function () { + try { + new TokenModel(); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `accessToken`"); + } + }); + it("throws, if `accessToken` is missing", function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + client: "bar", + user: "tar", + accessTokenExpiresAt: atExpiresAt, + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `accessToken`"); + } + }); + it("throws, if `client` is missing", function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: "foo", + user: "tar", + accessTokenExpiresAt: atExpiresAt, + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `client`"); + } + }); + it("throws, if `user` is missing", function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: "foo", + client: "bar", + accessTokenExpiresAt: atExpiresAt, + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Missing parameter: `user`"); + } + }); + it("throws, if `accessTokenExpiresAt` is not a Date", function () { + const data = { + accessToken: "foo", + client: "bar", + user: "tar", + accessTokenExpiresAt: "11/10/2023", + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Invalid parameter: `accessTokenExpiresAt`"); + } + }); + it("throws, if `refreshTokenExpiresAt` is not a Date", function () { + const data = { + accessToken: "foo", + client: "bar", + user: "tar", + refreshTokenExpiresAt: "11/10/2023", + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal("Invalid parameter: `refreshTokenExpiresAt`"); + } + }); + it("should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set", function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: "foo", + client: "bar", + user: "tar", + accessTokenExpiresAt: atExpiresAt, + }; + + const model = new TokenModel(data); + should.exist(model.accessTokenLifetime); + model.accessTokenLifetime.should.a("number"); + model.accessTokenLifetime.should.be.approximately(3600, 2); + }); + + it("should throw if the required arguments are not provided", () => { + should.throw(() => { + new TokenModel({}); + }); + }); + + it("should ignore custom attributes if allowExtendedTokenAttributes is not specified as true", () => { + const model = new TokenModel({ + accessToken: "token", + client: "client", + user: "user", + myCustomAttribute: "myCustomValue", + }); + + should.not.exist(model["myCustomAttribute"]); + should.not.exist(model["customAttributes"]); + }); + + it("should set custom attributes on the customAttributes field if allowExtendedTokenAttributes is specified as true", () => { + const model = new TokenModel( + { + accessToken: "token", + client: "client", + user: "user", + myCustomAttribute: "myCustomValue", + }, + { + allowExtendedTokenAttributes: true, + }, + ); + + should.not.exist(model["myCustomAttribute"]); + model["customAttributes"].should.be.an("object"); + model["customAttributes"]["myCustomAttribute"].should.equal( + "myCustomValue", + ); + }); + }); }); diff --git a/test/unit/pkce/pkce_test.js b/test/unit/pkce/pkce_test.js index 363eb8db..c5744cae 100644 --- a/test/unit/pkce/pkce_test.js +++ b/test/unit/pkce/pkce_test.js @@ -1,99 +1,121 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const pkce = require('../../../lib/pkce/pkce'); -const should = require('chai').should(); -const { base64URLEncode } = require('../../../lib/utils/string-util'); -const { createHash } = require('../../../lib/utils/crypto-util'); +const pkce = require("../../../lib/pkce/pkce"); +const should = require("chai").should(); +const { base64URLEncode } = require("../../../lib/utils/string-util"); +const { createHash } = require("../../../lib/utils/crypto-util"); -describe('PKCE', function() { - describe(pkce.isPKCERequest.name, function () { - it('returns, whether parameters define a PKCE request', function () { - [ - [true, 'authorization_code', 'foo'], - [true, 'authorization_code', '123123123123123123123123123123123123123123123'], - [false, 'authorization_code', ''], - [false, 'authorization_code', undefined], - [false, 'foo_code', '123123123123123123123123123123123123123123123'], - [false, '', '123123123123123123123123123123123123123123123'], - [false, undefined, '123123123123123123123123123123123123123123123'], - [false, 'foo_code', 'bar'] - ].forEach(triple => { - should.equal(triple[0], pkce.isPKCERequest({ - grantType: triple[1], - codeVerifier: triple[2] - })); - }); - }); - }); - describe(pkce.codeChallengeMatchesABNF.name, function () { - it('returns whether a string matches the criteria for codeChallenge', function () { - [ - [false, undefined], - [false, null], - [false, ''], - [false, '123123123112312312311231231231123123123112'], // too short - [false, '123123123112312312311231231231123123123112+'], // invalid chars - [false, '123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123'], // too long - // invalid chars - [true, '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'], - ].forEach(pair => { - should.equal(pair[0], pkce.codeChallengeMatchesABNF(pair[1])); - }); - }); - }); - describe(pkce.getHashForCodeChallenge.name, function () { - it('returns nothing if method is not valid', function () { - const verifier = '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; +describe("PKCE", function () { + describe(pkce.isPKCERequest.name, function () { + it("returns, whether parameters define a PKCE request", function () { + [ + [true, "authorization_code", "foo"], + [ + true, + "authorization_code", + "123123123123123123123123123123123123123123123", + ], + [false, "authorization_code", ""], + [false, "authorization_code", undefined], + [false, "foo_code", "123123123123123123123123123123123123123123123"], + [false, "", "123123123123123123123123123123123123123123123"], + [false, undefined, "123123123123123123123123123123123123123123123"], + [false, "foo_code", "bar"], + ].forEach((triple) => { + should.equal( + triple[0], + pkce.isPKCERequest({ + grantType: triple[1], + codeVerifier: triple[2], + }), + ); + }); + }); + }); + describe(pkce.codeChallengeMatchesABNF.name, function () { + it("returns whether a string matches the criteria for codeChallenge", function () { + [ + [false, undefined], + [false, null], + [false, ""], + [false, "123123123112312312311231231231123123123112"], // too short + [false, "123123123112312312311231231231123123123112+"], // invalid chars + [ + false, + "123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123", + ], // too long + // invalid chars + [ + true, + "-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ", + ], + ].forEach((pair) => { + should.equal(pair[0], pkce.codeChallengeMatchesABNF(pair[1])); + }); + }); + }); + describe(pkce.getHashForCodeChallenge.name, function () { + it("returns nothing if method is not valid", function () { + const verifier = + "-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ"; - [ - [undefined, undefined, verifier], - [undefined, null, verifier], - [undefined, '', verifier], - [undefined, 'foo', verifier], - ].forEach(triple => { - should.equal(triple[0], pkce.getHashForCodeChallenge({ - method: triple[1], - verifier: triple[2], - })); - }); - }); - it('return the verifier on plain and undefined on S256 if verifier is falsy', function () { - [ - [undefined, 'plain', undefined], - [undefined, 'S256', undefined], - [undefined, 'plain', ''], - [undefined, 'S256', ''], - [undefined, 'plain', null], - [undefined, 'S256', null], - ].forEach(triple => { - should.equal(triple[0], pkce.getHashForCodeChallenge({ - method: triple[1], - verifier: triple[2], - })); - }); - }); - it('returns the unhashed verifier when method is plain', function () { - const verifier = '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; - const hash = pkce.getHashForCodeChallenge({ method: 'plain', verifier }); - should.equal(hash, verifier); - }); - it('returns the hash verifier when method is S256', function () { - const verifier = '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; - const hash = pkce.getHashForCodeChallenge({ method: 'S256', verifier }); - const expectedHash = base64URLEncode(createHash({ data: verifier })); - should.equal(hash, expectedHash); - }); - }); - describe(pkce.isValidMethod.name, function () { - it('returns if a method is plain or S256', function () { - should.equal(pkce.isValidMethod('plain'), true); - should.equal(pkce.isValidMethod('S256'), true); - should.equal(pkce.isValidMethod('foo'), false); - should.equal(pkce.isValidMethod(), false); - }); - }); + [ + [undefined, undefined, verifier], + [undefined, null, verifier], + [undefined, "", verifier], + [undefined, "foo", verifier], + ].forEach((triple) => { + should.equal( + triple[0], + pkce.getHashForCodeChallenge({ + method: triple[1], + verifier: triple[2], + }), + ); + }); + }); + it("return the verifier on plain and undefined on S256 if verifier is falsy", function () { + [ + [undefined, "plain", undefined], + [undefined, "S256", undefined], + [undefined, "plain", ""], + [undefined, "S256", ""], + [undefined, "plain", null], + [undefined, "S256", null], + ].forEach((triple) => { + should.equal( + triple[0], + pkce.getHashForCodeChallenge({ + method: triple[1], + verifier: triple[2], + }), + ); + }); + }); + it("returns the unhashed verifier when method is plain", function () { + const verifier = + "-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ"; + const hash = pkce.getHashForCodeChallenge({ method: "plain", verifier }); + should.equal(hash, verifier); + }); + it("returns the hash verifier when method is S256", function () { + const verifier = + "-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ"; + const hash = pkce.getHashForCodeChallenge({ method: "S256", verifier }); + const expectedHash = base64URLEncode(createHash({ data: verifier })); + should.equal(hash, expectedHash); + }); + }); + describe(pkce.isValidMethod.name, function () { + it("returns if a method is plain or S256", function () { + should.equal(pkce.isValidMethod("plain"), true); + should.equal(pkce.isValidMethod("S256"), true); + should.equal(pkce.isValidMethod("foo"), false); + should.equal(pkce.isValidMethod(), false); + }); + }); }); diff --git a/test/unit/request_test.js b/test/unit/request_test.js index ee5a2761..73b4bce6 100644 --- a/test/unit/request_test.js +++ b/test/unit/request_test.js @@ -1,203 +1,215 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const Request = require('../../lib/request'); -const InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); -const should = require('chai').should(); +const Request = require("../../lib/request"); +const InvalidArgumentError = require("../../lib/errors/invalid-argument-error"); +const should = require("chai").should(); /** * Test `Request`. */ function generateBaseRequest() { - return { - query: { - foo: 'bar' - }, - method: 'GET', - headers: { - bar: 'foo' - }, - body: { - foobar: 'barfoo' - } - }; + return { + query: { + foo: "bar", + }, + method: "GET", + headers: { + bar: "foo", + }, + body: { + foobar: "barfoo", + }, + }; } -describe('Request', function() { - it('should throw on missing args', function () { - const args = [ - [undefined, InvalidArgumentError, 'Missing parameter: `headers`'], - [null, TypeError, 'Cannot destructure property \'headers\''], - [{}, InvalidArgumentError, 'Missing parameter: `headers`'], - [{ headers: { }}, InvalidArgumentError, 'Missing parameter: `method`'], - [{ headers: {}, method: 'GET' }, InvalidArgumentError, 'Missing parameter: `query`'], - ]; - - args.forEach(([value, error, message]) => { - try { - new Request(value); - } catch (e) { - e.should.be.instanceOf(error); - e.message.should.include(message); - } - }); - }); - it('should instantiate with a basic request', function() { - const originalRequest = generateBaseRequest(); - - const request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - }); - - it('should allow a request to be passed without a body', function() { - const originalRequest = generateBaseRequest(); - delete originalRequest.body; - - const request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql({}); - }); - - it('should throw if headers are not passed to the constructor', function() { - const originalRequest = generateBaseRequest(); - delete originalRequest.headers; - - (function() { - new Request(originalRequest); - }).should.throw('Missing parameter: `headers`'); - }); - - it('should throw if query string isn\'t passed to the constructor', function() { - const originalRequest = generateBaseRequest(); - delete originalRequest.query; - - (function() { - new Request(originalRequest); - }).should.throw('Missing parameter: `query`'); - }); - - it('should throw if method isn\'t passed to the constructor', function() { - const originalRequest = generateBaseRequest(); - delete originalRequest.method; - - (function() { - new Request(originalRequest); - }).should.throw('Missing parameter: `method`'); - }); - - it('should convert all header keys to lowercase', function() { - const originalRequest = generateBaseRequest(); - originalRequest.headers = { - Foo: 'bar', - BAR: 'foo' - }; - - const request = new Request(originalRequest); - request.headers.foo.should.eql('bar'); - request.headers.bar.should.eql('foo'); - should.not.exist(request.headers.Foo); - should.not.exist(request.headers.BAR); - }); - - it('should include additional properties passed in the request', function() { - const originalRequest = generateBaseRequest(); - originalRequest.custom = { - newFoo: 'newBar' - }; - - originalRequest.custom2 = { - newBar: 'newFoo' - }; - - const request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - request.custom.should.eql(originalRequest.custom); - request.custom2.should.eql(originalRequest.custom2); - }); - - it('should include additional properties passed in the request', function() { - const originalRequest = generateBaseRequest(); - originalRequest.custom = { - newFoo: 'newBar' - }; - - originalRequest.custom2 = { - newBar: 'newFoo' - }; - - const request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - request.custom.should.eql(originalRequest.custom); - request.custom2.should.eql(originalRequest.custom2); - }); - - it('should not allow overwriting methods on the Request prototype via custom properties', () => { - const request = new Request({ - query: {}, - method: 'GET', - headers: { - 'content-type': 'application/json' - }, - get() { - // malicious attempt to override the 'get' method - return 'text/html'; - } - }); - - request.get('content-type').should.equal('application/json'); - }); - - it('should allow getting of headers using `request.get`', function() { - const originalRequest = generateBaseRequest(); - - const request = new Request(originalRequest); - request.get('bar').should.eql(originalRequest.headers.bar); - }); - - it('should allow getting of headers using `request.get`', function() { - const originalRequest = generateBaseRequest(); - - const request = new Request(originalRequest); - request.get('bar').should.eql(originalRequest.headers.bar); - }); - - it('should allow getting of headers using `request.get`', function() { - const originalRequest = generateBaseRequest(); - - const request = new Request(originalRequest); - request.get('bar').should.eql(originalRequest.headers.bar); - }); - - it('should validate the content-type', function() { - const originalRequest = generateBaseRequest(); - originalRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; - originalRequest.headers['content-length'] = JSON.stringify(originalRequest.body).length; - - const request = new Request(originalRequest); - request.is('application/x-www-form-urlencoded').should.eql('application/x-www-form-urlencoded'); - }); - - it('should return false if the content-type is invalid', function() { - const originalRequest = generateBaseRequest(); - originalRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; - originalRequest.headers['content-length'] = JSON.stringify(originalRequest.body).length; - - const request = new Request(originalRequest); - request.is('application/json').should.eql(false); - }); +describe("Request", function () { + it("should throw on missing args", function () { + const args = [ + [undefined, InvalidArgumentError, "Missing parameter: `headers`"], + [null, TypeError, "Cannot destructure property 'headers'"], + [{}, InvalidArgumentError, "Missing parameter: `headers`"], + [{ headers: {} }, InvalidArgumentError, "Missing parameter: `method`"], + [ + { headers: {}, method: "GET" }, + InvalidArgumentError, + "Missing parameter: `query`", + ], + ]; + + args.forEach(([value, error, message]) => { + try { + new Request(value); + } catch (e) { + e.should.be.instanceOf(error); + e.message.should.include(message); + } + }); + }); + it("should instantiate with a basic request", function () { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + }); + + it("should allow a request to be passed without a body", function () { + const originalRequest = generateBaseRequest(); + delete originalRequest.body; + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql({}); + }); + + it("should throw if headers are not passed to the constructor", function () { + const originalRequest = generateBaseRequest(); + delete originalRequest.headers; + + (function () { + new Request(originalRequest); + }).should.throw("Missing parameter: `headers`"); + }); + + it("should throw if query string isn't passed to the constructor", function () { + const originalRequest = generateBaseRequest(); + delete originalRequest.query; + + (function () { + new Request(originalRequest); + }).should.throw("Missing parameter: `query`"); + }); + + it("should throw if method isn't passed to the constructor", function () { + const originalRequest = generateBaseRequest(); + delete originalRequest.method; + + (function () { + new Request(originalRequest); + }).should.throw("Missing parameter: `method`"); + }); + + it("should convert all header keys to lowercase", function () { + const originalRequest = generateBaseRequest(); + originalRequest.headers = { + Foo: "bar", + BAR: "foo", + }; + + const request = new Request(originalRequest); + request.headers.foo.should.eql("bar"); + request.headers.bar.should.eql("foo"); + should.not.exist(request.headers.Foo); + should.not.exist(request.headers.BAR); + }); + + it("should include additional properties passed in the request", function () { + const originalRequest = generateBaseRequest(); + originalRequest.custom = { + newFoo: "newBar", + }; + + originalRequest.custom2 = { + newBar: "newFoo", + }; + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + request.custom.should.eql(originalRequest.custom); + request.custom2.should.eql(originalRequest.custom2); + }); + + it("should include additional properties passed in the request", function () { + const originalRequest = generateBaseRequest(); + originalRequest.custom = { + newFoo: "newBar", + }; + + originalRequest.custom2 = { + newBar: "newFoo", + }; + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + request.custom.should.eql(originalRequest.custom); + request.custom2.should.eql(originalRequest.custom2); + }); + + it("should not allow overwriting methods on the Request prototype via custom properties", () => { + const request = new Request({ + query: {}, + method: "GET", + headers: { + "content-type": "application/json", + }, + get() { + // malicious attempt to override the 'get' method + return "text/html"; + }, + }); + + request.get("content-type").should.equal("application/json"); + }); + + it("should allow getting of headers using `request.get`", function () { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get("bar").should.eql(originalRequest.headers.bar); + }); + + it("should allow getting of headers using `request.get`", function () { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get("bar").should.eql(originalRequest.headers.bar); + }); + + it("should allow getting of headers using `request.get`", function () { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get("bar").should.eql(originalRequest.headers.bar); + }); + + it("should validate the content-type", function () { + const originalRequest = generateBaseRequest(); + originalRequest.headers["content-type"] = + "application/x-www-form-urlencoded"; + originalRequest.headers["content-length"] = JSON.stringify( + originalRequest.body, + ).length; + + const request = new Request(originalRequest); + request + .is("application/x-www-form-urlencoded") + .should.eql("application/x-www-form-urlencoded"); + }); + + it("should return false if the content-type is invalid", function () { + const originalRequest = generateBaseRequest(); + originalRequest.headers["content-type"] = + "application/x-www-form-urlencoded"; + originalRequest.headers["content-length"] = JSON.stringify( + originalRequest.body, + ).length; + + const request = new Request(originalRequest); + request.is("application/json").should.eql(false); + }); }); diff --git a/test/unit/response_test.js b/test/unit/response_test.js index af505ba9..34644188 100644 --- a/test/unit/response_test.js +++ b/test/unit/response_test.js @@ -1,134 +1,134 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const Response = require('../../lib/response'); -const should = require('chai').should(); +const Response = require("../../lib/response"); +const should = require("chai").should(); /** * Test `Request`. */ function generateBaseResponse() { - return { - headers: { - bar: 'foo' - }, - body: { - foobar: 'barfoo' - } - }; + return { + headers: { + bar: "foo", + }, + body: { + foobar: "barfoo", + }, + }; } -describe('Request', function() { - it('should instantiate with a basic request', function() { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql(originalResponse.body); - response.status.should.eql(200); - }); - - it('should allow a response to be passed without a body', function() { - const originalResponse = generateBaseResponse(); - delete originalResponse.body; - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql({}); - response.status.should.eql(200); - }); - - it('should allow a response to be passed without headers', function() { - const originalResponse = generateBaseResponse(); - delete originalResponse.headers; - - const response = new Response(originalResponse); - response.headers.should.eql({}); - response.body.should.eql(originalResponse.body); - response.status.should.eql(200); - }); - - it('should convert all header keys to lowercase', function() { - const originalResponse = generateBaseResponse(); - originalResponse.headers = { - Foo: 'bar', - BAR: 'foo' - }; - - const response = new Response(originalResponse); - response.headers.foo.should.eql('bar'); - response.headers.bar.should.eql('foo'); - should.not.exist(response.headers.Foo); - should.not.exist(response.headers.BAR); - }); - - it('should include additional properties passed in the response', function() { - const originalResponse = generateBaseResponse(); - originalResponse.custom = { - newFoo: 'newBar' - }; - - originalResponse.custom2 = { - newBar: 'newFoo' - }; - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql(originalResponse.body); - response.custom.should.eql(originalResponse.custom); - response.custom2.should.eql(originalResponse.custom2); - }); - - it('should not allow overwriting methods on the Response prototype via custom properties', () => { - const response = new Response({ - headers: { - 'content-type': 'application/json' - }, - get() { - // malicious attempt to override the 'get' method - return 'text/html'; - } - }); - - response.get('content-type').should.equal('application/json'); - }); - - it('should allow getting of headers using `response.get`', function() { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.get('bar').should.eql(originalResponse.headers.bar); - }); - - it('should allow getting of headers using `response.get`', function() { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.get('bar').should.eql(originalResponse.headers.bar); - }); - - it('should allow setting of headers using `response.set`', function() { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.set('newheader', 'newvalue'); - response.headers.bar.should.eql('foo'); - response.headers.newheader.should.eql('newvalue'); - }); - - it('should process redirect', function() { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.status.should.eql(200); - response.redirect('http://foo.bar'); - response.headers.location.should.eql('http://foo.bar'); - response.status.should.eql(302); - }); +describe("Request", function () { + it("should instantiate with a basic request", function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql(originalResponse.body); + response.status.should.eql(200); + }); + + it("should allow a response to be passed without a body", function () { + const originalResponse = generateBaseResponse(); + delete originalResponse.body; + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql({}); + response.status.should.eql(200); + }); + + it("should allow a response to be passed without headers", function () { + const originalResponse = generateBaseResponse(); + delete originalResponse.headers; + + const response = new Response(originalResponse); + response.headers.should.eql({}); + response.body.should.eql(originalResponse.body); + response.status.should.eql(200); + }); + + it("should convert all header keys to lowercase", function () { + const originalResponse = generateBaseResponse(); + originalResponse.headers = { + Foo: "bar", + BAR: "foo", + }; + + const response = new Response(originalResponse); + response.headers.foo.should.eql("bar"); + response.headers.bar.should.eql("foo"); + should.not.exist(response.headers.Foo); + should.not.exist(response.headers.BAR); + }); + + it("should include additional properties passed in the response", function () { + const originalResponse = generateBaseResponse(); + originalResponse.custom = { + newFoo: "newBar", + }; + + originalResponse.custom2 = { + newBar: "newFoo", + }; + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql(originalResponse.body); + response.custom.should.eql(originalResponse.custom); + response.custom2.should.eql(originalResponse.custom2); + }); + + it("should not allow overwriting methods on the Response prototype via custom properties", () => { + const response = new Response({ + headers: { + "content-type": "application/json", + }, + get() { + // malicious attempt to override the 'get' method + return "text/html"; + }, + }); + + response.get("content-type").should.equal("application/json"); + }); + + it("should allow getting of headers using `response.get`", function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.get("bar").should.eql(originalResponse.headers.bar); + }); + + it("should allow getting of headers using `response.get`", function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.get("bar").should.eql(originalResponse.headers.bar); + }); + + it("should allow setting of headers using `response.set`", function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.set("newheader", "newvalue"); + response.headers.bar.should.eql("foo"); + response.headers.newheader.should.eql("newvalue"); + }); + + it("should process redirect", function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.status.should.eql(200); + response.redirect("http://foo.bar"); + response.headers.location.should.eql("http://foo.bar"); + response.status.should.eql(302); + }); }); diff --git a/test/unit/server_test.js b/test/unit/server_test.js index f8caa2bc..aaa08c77 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -1,72 +1,78 @@ -'use strict'; +"use strict"; /** * Module dependencies. */ -const AuthenticateHandler = require('../../lib/handlers/authenticate-handler'); -const AuthorizeHandler = require('../../lib/handlers/authorize-handler'); -const Server = require('../../lib/server'); -const TokenHandler = require('../../lib/handlers/token-handler'); -const Model = require('../../lib/model'); -const sinon = require('sinon'); +const AuthenticateHandler = require("../../lib/handlers/authenticate-handler"); +const AuthorizeHandler = require("../../lib/handlers/authorize-handler"); +const Server = require("../../lib/server"); +const TokenHandler = require("../../lib/handlers/token-handler"); +const Model = require("../../lib/model"); +const sinon = require("sinon"); /** * Test `Server`. */ -describe('Server', function() { - describe('authenticate()', function() { - it('should call `handle`', function() { - const model = Model.from({ - getAccessToken: function() {} - }); - const server = new Server({ model: model }); +describe("Server", function () { + describe("authenticate()", function () { + it("should call `handle`", function () { + const model = Model.from({ + getAccessToken: function () {}, + }); + const server = new Server({ model: model }); - sinon.stub(AuthenticateHandler.prototype, 'handle').returns(Promise.resolve()); + sinon + .stub(AuthenticateHandler.prototype, "handle") + .returns(Promise.resolve()); - server.authenticate('foo'); + server.authenticate("foo"); - AuthenticateHandler.prototype.handle.callCount.should.equal(1); - AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - AuthenticateHandler.prototype.handle.restore(); - }); - }); + AuthenticateHandler.prototype.handle.callCount.should.equal(1); + AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal( + "foo", + ); + AuthenticateHandler.prototype.handle.restore(); + }); + }); - describe('authorize()', function() { - it('should call `handle`', function() { - const model = Model.from({ - getAccessToken: function() {}, - getClient: function() {}, - saveAuthorizationCode: function() {} - }); - const server = new Server({ model: model }); + describe("authorize()", function () { + it("should call `handle`", function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const server = new Server({ model: model }); - sinon.stub(AuthorizeHandler.prototype, 'handle').returns(Promise.resolve()); + sinon + .stub(AuthorizeHandler.prototype, "handle") + .returns(Promise.resolve()); - server.authorize('foo', 'bar'); + server.authorize("foo", "bar"); - AuthorizeHandler.prototype.handle.callCount.should.equal(1); - AuthorizeHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - AuthorizeHandler.prototype.handle.restore(); - }); - }); + AuthorizeHandler.prototype.handle.callCount.should.equal(1); + AuthorizeHandler.prototype.handle.firstCall.args[0].should.equal("foo"); + AuthorizeHandler.prototype.handle.restore(); + }); + }); - describe('token()', function() { - it('should call `handle`', function() { - const model = Model.from({ - getClient: function() {}, - saveToken: function() {} - }); - const server = new Server({ model: model }); + describe("token()", function () { + it("should call `handle`", function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const server = new Server({ model: model }); - sinon.stub(TokenHandler.prototype, 'handle').returns(Promise.resolve()); + sinon.stub(TokenHandler.prototype, "handle").returns(Promise.resolve()); - server.token('foo', 'bar'); + server.token("foo", "bar"); - TokenHandler.prototype.handle.callCount.should.equal(1); - TokenHandler.prototype.handle.firstCall.args[0].should.equal('foo'); - TokenHandler.prototype.handle.restore(); - }); - }); + TokenHandler.prototype.handle.callCount.should.equal(1); + TokenHandler.prototype.handle.firstCall.args[0].should.equal("foo"); + TokenHandler.prototype.handle.restore(); + }); + }); }); diff --git a/test/unit/utils/crypto-util_test.js b/test/unit/utils/crypto-util_test.js index ff7c8444..04198711 100644 --- a/test/unit/utils/crypto-util_test.js +++ b/test/unit/utils/crypto-util_test.js @@ -1,18 +1,22 @@ -const cryptoUtil = require('../../../lib/utils/crypto-util'); -require('chai').should(); +const cryptoUtil = require("../../../lib/utils/crypto-util"); +require("chai").should(); describe(cryptoUtil.createHash.name, function () { - it('creates a hash by given algorithm', function () { - const data = 'client-credentials-grant'; - const hash = cryptoUtil.createHash({ data, output: 'hex' }); - hash.should.equal('072726830f0aadd2d91f86f53e3a7ef40018c2626438152dd576e272bf2b8e60'); - }); - it('should throw if data is missing', function () { - try { - cryptoUtil.createHash({}); - } catch (e) { - e.should.be.instanceOf(TypeError); - e.message.should.include('he "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.'); - } - }); + it("creates a hash by given algorithm", function () { + const data = "client-credentials-grant"; + const hash = cryptoUtil.createHash({ data, output: "hex" }); + hash.should.equal( + "072726830f0aadd2d91f86f53e3a7ef40018c2626438152dd576e272bf2b8e60", + ); + }); + it("should throw if data is missing", function () { + try { + cryptoUtil.createHash({}); + } catch (e) { + e.should.be.instanceOf(TypeError); + e.message.should.include( + 'he "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.', + ); + } + }); }); diff --git a/test/unit/utils/date-util__test.js b/test/unit/utils/date-util__test.js index 47b8e7d5..36e82081 100644 --- a/test/unit/utils/date-util__test.js +++ b/test/unit/utils/date-util__test.js @@ -1,26 +1,26 @@ -const dateUtil = require('../../../lib/utils/date-util'); +const dateUtil = require("../../../lib/utils/date-util"); -const sinon = require('sinon'); -require('chai').should(); +const sinon = require("sinon"); +require("chai").should(); -describe('DateUtil', function() { - describe('getLifetimeFromExpiresAt', () => { - const now = new Date('2023-01-01T00:00:00.000Z'); +describe("DateUtil", function () { + describe("getLifetimeFromExpiresAt", () => { + const now = new Date("2023-01-01T00:00:00.000Z"); - beforeEach(() => { - sinon.useFakeTimers(now); - }); + beforeEach(() => { + sinon.useFakeTimers(now); + }); - it('should convert a valid expiration date into seconds from now', () => { - const expiresAt = new Date('2023-01-01T00:00:10.000Z'); - const lifetime = dateUtil.getLifetimeFromExpiresAt(expiresAt); + it("should convert a valid expiration date into seconds from now", () => { + const expiresAt = new Date("2023-01-01T00:00:10.000Z"); + const lifetime = dateUtil.getLifetimeFromExpiresAt(expiresAt); - lifetime.should.be.a('number'); - lifetime.should.be.approximately(10, 2); - }); + lifetime.should.be.a("number"); + lifetime.should.be.approximately(10, 2); + }); - afterEach(() => { - sinon.restore(); - }); - }); + afterEach(() => { + sinon.restore(); + }); + }); }); diff --git a/test/unit/utils/scope-util_test.js b/test/unit/utils/scope-util_test.js index 505262f5..128672f0 100644 --- a/test/unit/utils/scope-util_test.js +++ b/test/unit/utils/scope-util_test.js @@ -1,45 +1,55 @@ -const { parseScope } = require('../../../lib/utils/scope-util'); -const should = require('chai').should(); +const { parseScope } = require("../../../lib/utils/scope-util"); +const should = require("chai").should(); describe(parseScope.name, () => { - it('should return undefined on nullish values', () => { - const values = [undefined, null]; - values.forEach(str => { - const compare = parseScope(str) === undefined; - compare.should.equal(true); - }); - }); - it('should throw on non-string values', () => { - const invalid = [1, -1, true, false, {}, ['foo'], [], () => {}, Symbol('foo')]; - invalid.forEach(str => { - try { - parseScope(str); - should.fail(); - } catch (e) { - e.message.should.eql('Invalid parameter: `scope`'); - } - }); - }); - it('should throw on empty strings', () => { - const invalid = ['', ' ', ' ', '\n', '\t', '\r']; - invalid.forEach(str => { - try { - parseScope(str); - should.fail(); - } catch (e) { - e.message.should.eql('Invalid parameter: `scope`'); - } - }); - }); - it('should split space-delimited strings into arrays', () => { - const values = [ - ['foo', ['foo']], - ['foo bar', ['foo', 'bar']], - ['foo bar', ['foo', 'bar']], - ]; - values.forEach(([str, compare]) => { - const parsed = parseScope(str); - parsed.should.deep.equal(compare); - }); - }); -}); \ No newline at end of file + it("should return undefined on nullish values", () => { + const values = [undefined, null]; + values.forEach((str) => { + const compare = parseScope(str) === undefined; + compare.should.equal(true); + }); + }); + it("should throw on non-string values", () => { + const invalid = [ + 1, + -1, + true, + false, + {}, + ["foo"], + [], + () => {}, + Symbol("foo"), + ]; + invalid.forEach((str) => { + try { + parseScope(str); + should.fail(); + } catch (e) { + e.message.should.eql("Invalid parameter: `scope`"); + } + }); + }); + it("should throw on empty strings", () => { + const invalid = ["", " ", " ", "\n", "\t", "\r"]; + invalid.forEach((str) => { + try { + parseScope(str); + should.fail(); + } catch (e) { + e.message.should.eql("Invalid parameter: `scope`"); + } + }); + }); + it("should split space-delimited strings into arrays", () => { + const values = [ + ["foo", ["foo"]], + ["foo bar", ["foo", "bar"]], + ["foo bar", ["foo", "bar"]], + ]; + values.forEach(([str, compare]) => { + const parsed = parseScope(str); + parsed.should.deep.equal(compare); + }); + }); +}); From 440e58e23206e3c8b79fbb4f593c25e72fc0f1cd Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Mon, 15 Jun 2026 10:32:56 +0200 Subject: [PATCH 03/11] fix: run biome formatter --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f01c4af..93599574 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ Complete, compliant and well tested module for implementing an OAuth2 server in [![Tests](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests.yml) [![CodeQL Semantic Analysis](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/codeql-analysis.yml) -[![Tests for Release](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests-release.yml/badge.svg)](https://github.com/node-oauth/node-oauth2-server/actions/workflows/tests-release.yml) [![Documentation Status](https://readthedocs.org/projects/node-oauthoauth2-server/badge/?version=latest)](https://node-oauthoauth2-server.readthedocs.io/en/latest/?badge=latest) [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) [![npm Version](https://img.shields.io/npm/v/@node-oauth/oauth2-server?label=version)](https://www.npmjs.com/package/@node-oauth/oauth2-server) [![npm Downloads/Week](https://img.shields.io/npm/dw/@node-oauth/oauth2-server)](https://www.npmjs.com/package/@node-oauth/oauth2-server) [![GitHub License](https://img.shields.io/github/license/node-oauth/node-oauth2-server)](https://github.com/node-oauth/node-oauth2-server/blob/master/LICENSE) +[![Formatted with Biome](https://img.shields.io/badge/Formatted_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev/) NOTE: This project has been forked from [oauthjs/node-oauth2-server](https://github.com/oauthjs/node-oauth2-server) and is a continuation due to the project appearing to be abandoned. Please see [our issue board](https://github.com/node-oauth/node-oauth2-server/issues) to talk about next steps and the future of this project. From f4e9079f133026d868e8b91d99922e640be66216 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Mon, 15 Jun 2026 10:39:32 +0200 Subject: [PATCH 04/11] fix: biome formatter fix --- package.json | 4 +- .../grant-types/abstract-grant-type_test.js | 12 ++-- .../authorization-code-grant-type_test.js | 14 ++--- .../refresh-token-grant-type_test.js | 56 ++++++++----------- test/unit/handlers/authorize-handler_test.js | 10 ++-- 5 files changed, 41 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 3efd286d..ba21fcd0 100644 --- a/package.json +++ b/package.json @@ -49,9 +49,9 @@ "test-debug": "NODE_ENV=test ./node_modules/.bin/mocha --inspect --debug-brk 'test/**/*_test.js'", "test:watch": "NODE_ENV=test ./node_modules/.bin/mocha --watch 'test/**/*_test.js'", "test:coverage": "NODE_ENV=test nyc --reporter=html --reporter=lcov --reporter=text ./node_modules/.bin/mocha 'test/**/*_test.js'", - "lint": "npx @biomejs/biome lint --diagnostic-level=warn .", + "lint": "npx @biomejs/biome lint .", "lint:fix": "npx @biomejs/biome lint --write .", - "format": "npx @biomejs/biome format --diagnostic-level=warn .", + "format": "npx @biomejs/biome format .", "format:fix": "npx @biomejs/biome format --write .", "docs:dev": "vitepress dev docs", "docs:build": "npm run docs:setup && npm run docs:api && vitepress build docs", diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js index c1ea4d8c..432b3bb1 100644 --- a/test/unit/grant-types/abstract-grant-type_test.js +++ b/test/unit/grant-types/abstract-grant-type_test.js @@ -39,13 +39,11 @@ describe("AbstractGrantType", function () { describe("generateRefreshToken()", function () { it("should call `model.generateRefreshToken()`", function () { const model = Model.from({ - generateRefreshToken: sinon - .stub() - .returns({ - client: {}, - expiresAt: new Date(new Date() / 2), - user: {}, - }), + generateRefreshToken: sinon.stub().returns({ + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }), }); const handler = new AbstractGrantType({ accessTokenLifetime: 120, diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index f752cf87..df7af5a0 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -20,14 +20,12 @@ describe("AuthorizationCodeGrantType", function () { describe("getAuthorizationCode()", function () { it("should call `model.getAuthorizationCode()`", function () { const model = Model.from({ - getAuthorizationCode: sinon - .stub() - .returns({ - authorizationCode: 12345, - client: {}, - expiresAt: new Date(new Date() * 2), - user: {}, - }), + getAuthorizationCode: sinon.stub().returns({ + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() * 2), + user: {}, + }), revokeAuthorizationCode: function () {}, saveToken: function () {}, }); diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js index 9d919a43..4b0dce11 100644 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ b/test/unit/grant-types/refresh-token-grant-type_test.js @@ -25,14 +25,12 @@ describe("RefreshTokenGrantType", function () { saveToken: function () { return { accessToken: "bar", client: {}, user: {} }; }, - revokeToken: sinon - .stub() - .returns({ - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }), + revokeToken: sinon.stub().returns({ + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, @@ -95,14 +93,12 @@ describe("RefreshTokenGrantType", function () { it("should call `model.revokeToken()`", function () { const model = Model.from({ getRefreshToken: function () {}, - revokeToken: sinon - .stub() - .returns({ - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }), + revokeToken: sinon.stub().returns({ + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), saveToken: function () {}, }); const handler = new RefreshTokenGrantType({ @@ -125,14 +121,12 @@ describe("RefreshTokenGrantType", function () { it("should not call `model.revokeToken()`", function () { const model = Model.from({ getRefreshToken: function () {}, - revokeToken: sinon - .stub() - .returns({ - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }), + revokeToken: sinon.stub().returns({ + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), saveToken: function () {}, }); const handler = new RefreshTokenGrantType({ @@ -153,14 +147,12 @@ describe("RefreshTokenGrantType", function () { it("should not call `model.revokeToken()`", function () { const model = Model.from({ getRefreshToken: function () {}, - revokeToken: sinon - .stub() - .returns({ - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }), + revokeToken: sinon.stub().returns({ + accessToken: "foo", + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), saveToken: function () {}, }); const handler = new RefreshTokenGrantType({ diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 0d1046ad..8df8af4d 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -45,12 +45,10 @@ describe("AuthorizeHandler", function () { it("should call `model.getClient()`", function () { const model = Model.from({ getAccessToken: function () {}, - getClient: sinon - .stub() - .returns({ - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }), + getClient: sinon.stub().returns({ + grants: ["authorization_code"], + redirectUris: ["http://example.com/cb"], + }), saveAuthorizationCode: function () {}, }); const handler = new AuthorizeHandler({ From 6ce594019941951cc11af33492500aff041df491 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Mon, 15 Jun 2026 10:43:59 +0200 Subject: [PATCH 05/11] fix(tests): detect different line in emitted error after biome formatter fixes --- test/unit/errors/oauth-error_test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/errors/oauth-error_test.js b/test/unit/errors/oauth-error_test.js index 9792a9ae..28ade68a 100644 --- a/test/unit/errors/oauth-error_test.js +++ b/test/unit/errors/oauth-error_test.js @@ -53,7 +53,7 @@ describe("OAuthError", function () { e.stack.should.not.be.null; e.stack.should.not.be.undefined; e.stack.should.include("oauth-error_test.js"); - e.stack.should.include("40"); //error lineNUmber + e.stack.should.include("41"); //error lineNUmber } }); }); From a4caaa74e2bfd63e5eca7019e2c91327244c4c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=BCster?= Date: Tue, 16 Jun 2026 09:20:10 +0200 Subject: [PATCH 06/11] build(core): shut off use strict rule in biome.json Co-authored-by: Daniel Hensby --- biome.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index a0af7e51..e5e10ef7 100644 --- a/biome.json +++ b/biome.json @@ -16,7 +16,10 @@ "linter": { "enabled": true, "rules": { - "preset": "recommended" + "preset": "recommended", + "suspicious": { + "noRedundantUseStrict": "off" + } } }, "javascript": { From 45e385a5670ea4835e4b38f58570c59b3f6d5e7d Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Wed, 17 Jun 2026 08:37:39 +0200 Subject: [PATCH 07/11] fix(build): default format using single quote and space indent --- biome.json | 3 +- lib/errors/access-denied-error.js | 30 +- lib/errors/insufficient-scope-error.js | 30 +- lib/errors/invalid-argument-error.js | 30 +- lib/errors/invalid-client-error.js | 30 +- lib/errors/invalid-grant-error.js | 30 +- lib/errors/invalid-request-error.js | 20 +- lib/errors/invalid-scope-error.js | 20 +- lib/errors/invalid-token-error.js | 20 +- lib/errors/oauth-error.js | 74 +- lib/errors/server-error.js | 30 +- lib/errors/unauthorized-client-error.js | 20 +- lib/errors/unauthorized-request-error.js | 20 +- lib/errors/unsupported-grant-type-error.js | 20 +- lib/errors/unsupported-response-type-error.js | 20 +- lib/grant-types/abstract-grant-type.js | 244 +- .../authorization-code-grant-type.js | 626 +-- .../client-credentials-grant-type.js | 178 +- lib/grant-types/password-grant-type.js | 232 +- lib/grant-types/refresh-token-grant-type.js | 370 +- lib/handlers/authenticate-handler.js | 606 +-- lib/handlers/authorize-handler.js | 910 ++-- lib/handlers/token-handler.js | 656 +-- lib/model.js | 424 +- lib/models/token-model.js | 128 +- lib/pkce/pkce.js | 58 +- lib/request.js | 108 +- lib/response-types/code-response-type.js | 66 +- lib/response-types/token-response-type.js | 18 +- lib/response.js | 96 +- lib/server.js | 452 +- lib/token-types/bearer-token-type.js | 104 +- lib/token-types/mac-token-type.js | 18 +- lib/utils/crypto-util.js | 8 +- lib/utils/date-util.js | 6 +- lib/utils/scope-util.js | 30 +- lib/utils/string-util.js | 12 +- lib/utils/token-util.js | 24 +- test/assertions.js | 12 +- test/compliance/client-authentication_test.js | 204 +- .../client-credential-workflow_test.js | 220 +- test/compliance/password-grant-type_test.js | 336 +- test/compliance/pkce_test.js | 1298 +++--- .../refresh-token-grant-type_test.js | 270 +- test/helpers/db.js | 104 +- test/helpers/model.js | 188 +- test/helpers/request.js | 24 +- .../grant-types/abstract-grant-type_test.js | 594 +-- .../authorization-code-grant-type_test.js | 1888 ++++---- .../client-credentials-grant-type_test.js | 688 +-- .../grant-types/password-grant-type_test.js | 958 ++-- .../refresh-token-grant-type_test.js | 1560 +++---- .../handlers/authenticate-handler_test.js | 1790 ++++---- .../handlers/authorize-handler_test.js | 4002 ++++++++--------- .../handlers/token-handler_test.js | 3738 +++++++-------- test/integration/request_test.js | 356 +- .../response-types/code-response-type_test.js | 98 +- test/integration/response_test.js | 104 +- test/integration/server_test.js | 474 +- .../token-types/bearer-token-type_test.js | 164 +- test/integration/utils/token-util_test.js | 18 +- test/unit/errors/oauth-error_test.js | 96 +- .../grant-types/abstract-grant-type_test.js | 96 +- .../authorization-code-grant-type_test.js | 380 +- .../client-credentials-grant-type_test.js | 120 +- .../grant-types/password-grant-type_test.js | 144 +- .../refresh-token-grant-type_test.js | 520 +-- .../handlers/authenticate-handler_test.js | 372 +- test/unit/handlers/authorize-handler_test.js | 532 +-- test/unit/handlers/token-handler_test.js | 72 +- test/unit/models/model_wrapper_test.js | 60 +- test/unit/models/token-model_test.js | 310 +- test/unit/pkce/pkce_test.js | 226 +- test/unit/request_test.js | 402 +- test/unit/response_test.js | 240 +- test/unit/server_test.js | 112 +- test/unit/utils/crypto-util_test.js | 38 +- test/unit/utils/date-util__test.js | 38 +- test/unit/utils/scope-util_test.js | 104 +- 79 files changed, 14361 insertions(+), 14360 deletions(-) diff --git a/biome.json b/biome.json index e5e10ef7..9319e4b8 100644 --- a/biome.json +++ b/biome.json @@ -24,7 +24,8 @@ }, "javascript": { "formatter": { - "quoteStyle": "double" + "quoteStyle": "single", + "indentStyle": "space" } }, "assist": { diff --git a/lib/errors/access-denied-error.js b/lib/errors/access-denied-error.js index 847add28..d2c510cf 100644 --- a/lib/errors/access-denied-error.js +++ b/lib/errors/access-denied-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * @class @@ -12,20 +12,20 @@ const OAuthError = require("./oauth-error"); * @see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 */ class AccessDeniedError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 400, - name: "access_denied", - ...properties, - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 400, + name: 'access_denied', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = AccessDeniedError; diff --git a/lib/errors/insufficient-scope-error.js b/lib/errors/insufficient-scope-error.js index de96d878..82cf3bb3 100644 --- a/lib/errors/insufficient-scope-error.js +++ b/lib/errors/insufficient-scope-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * @class @@ -13,20 +13,20 @@ const OAuthError = require("./oauth-error"); */ class InsufficientScopeError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 403, - name: "insufficient_scope", - ...properties, - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 403, + name: 'insufficient_scope', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = InsufficientScopeError; diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js index 1030232a..3a1a4002 100644 --- a/lib/errors/invalid-argument-error.js +++ b/lib/errors/invalid-argument-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * @class @@ -12,20 +12,20 @@ const OAuthError = require("./oauth-error"); */ class InvalidArgumentError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 500, - name: "invalid_argument", - ...properties, - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 500, + name: 'invalid_argument', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = InvalidArgumentError; diff --git a/lib/errors/invalid-client-error.js b/lib/errors/invalid-client-error.js index acf32a90..2c18fabe 100644 --- a/lib/errors/invalid-client-error.js +++ b/lib/errors/invalid-client-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * @class @@ -14,20 +14,20 @@ const OAuthError = require("./oauth-error"); */ class InvalidClientError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 400, - name: "invalid_client", - ...properties, - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 400, + name: 'invalid_client', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = InvalidClientError; diff --git a/lib/errors/invalid-grant-error.js b/lib/errors/invalid-grant-error.js index edc2dc64..10e84693 100644 --- a/lib/errors/invalid-grant-error.js +++ b/lib/errors/invalid-grant-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * @class @@ -15,20 +15,20 @@ const OAuthError = require("./oauth-error"); */ class InvalidGrantError extends OAuthError { - /** - * @constructor - * @param message {string} - * @param properties {object=} - */ - constructor(message, properties) { - properties = { - code: 400, - name: "invalid_grant", - ...properties, - }; + /** + * @constructor + * @param message {string} + * @param properties {object=} + */ + constructor(message, properties) { + properties = { + code: 400, + name: 'invalid_grant', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } module.exports = InvalidGrantError; diff --git a/lib/errors/invalid-request-error.js b/lib/errors/invalid-request-error.js index 3035b0c5..15f87b56 100644 --- a/lib/errors/invalid-request-error.js +++ b/lib/errors/invalid-request-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * Constructor. @@ -16,15 +16,15 @@ const OAuthError = require("./oauth-error"); */ class InvalidRequest extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: "invalid_request", - ...properties, - }; + constructor(message, properties) { + properties = { + code: 400, + name: 'invalid_request', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js index 3211ab58..38aec9a1 100644 --- a/lib/errors/invalid-scope-error.js +++ b/lib/errors/invalid-scope-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * Constructor. @@ -15,15 +15,15 @@ const OAuthError = require("./oauth-error"); */ class InvalidScopeError extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: "invalid_scope", - ...properties, - }; + constructor(message, properties) { + properties = { + code: 400, + name: 'invalid_scope', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/invalid-token-error.js b/lib/errors/invalid-token-error.js index 9f397fd9..96469e44 100644 --- a/lib/errors/invalid-token-error.js +++ b/lib/errors/invalid-token-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * Constructor. @@ -15,15 +15,15 @@ const OAuthError = require("./oauth-error"); */ class InvalidTokenError extends OAuthError { - constructor(message, properties) { - properties = { - code: 401, - name: "invalid_token", - ...properties, - }; + constructor(message, properties) { + properties = { + code: 401, + name: 'invalid_token', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/oauth-error.js b/lib/errors/oauth-error.js index bab5e501..e9ee3ae9 100644 --- a/lib/errors/oauth-error.js +++ b/lib/errors/oauth-error.js @@ -1,50 +1,50 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const http = require("http"); +const http = require('http'); /** * Baseclass for OAuth related Error types. * @class */ class OAuthError extends Error { - /** - * @constructor - * @param messageOrError - * @param properties - */ - constructor(messageOrError, properties) { - super(messageOrError, properties); - - let message = - messageOrError instanceof Error ? messageOrError.message : messageOrError; - const error = messageOrError instanceof Error ? messageOrError : null; - - if (properties == null || !Object.entries(properties).length) { - properties = {}; - } - - properties = { code: 500, ...properties }; - - if (error) { - properties.inner = error; - } - - if (!message || message.length === 0) { - message = http.STATUS_CODES[properties.code]; - } - - this.code = this.status = this.statusCode = properties.code; - this.message = message; - - for (const key in properties) { - if (key !== "code") { - this[key] = properties[key]; - } - } - } + /** + * @constructor + * @param messageOrError + * @param properties + */ + constructor(messageOrError, properties) { + super(messageOrError, properties); + + let message = + messageOrError instanceof Error ? messageOrError.message : messageOrError; + const error = messageOrError instanceof Error ? messageOrError : null; + + if (properties == null || !Object.entries(properties).length) { + properties = {}; + } + + properties = { code: 500, ...properties }; + + if (error) { + properties.inner = error; + } + + if (!message || message.length === 0) { + message = http.STATUS_CODES[properties.code]; + } + + this.code = this.status = this.statusCode = properties.code; + this.message = message; + + for (const key in properties) { + if (key !== 'code') { + this[key] = properties[key]; + } + } + } } /** diff --git a/lib/errors/server-error.js b/lib/errors/server-error.js index f0df7603..0a87eaf1 100644 --- a/lib/errors/server-error.js +++ b/lib/errors/server-error.js @@ -1,11 +1,11 @@ -"use strict"; +'use strict'; /** * Module dependencies. * @private */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * "The authorization server encountered an unexpected condition that prevented it from fulfilling the request." @@ -14,20 +14,20 @@ const OAuthError = require("./oauth-error"); */ class ServerError extends OAuthError { - /** - * @constructor - * @param message - * @param properties - */ - constructor(message, properties) { - properties = { - code: 503, - name: "server_error", - ...properties, - }; + /** + * @constructor + * @param message + * @param properties + */ + constructor(message, properties) { + properties = { + code: 503, + name: 'server_error', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/unauthorized-client-error.js b/lib/errors/unauthorized-client-error.js index ab6ede29..a175ad69 100644 --- a/lib/errors/unauthorized-client-error.js +++ b/lib/errors/unauthorized-client-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * Constructor. @@ -15,15 +15,15 @@ const OAuthError = require("./oauth-error"); */ class UnauthorizedClientError extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: "unauthorized_client", - ...properties, - }; + constructor(message, properties) { + properties = { + code: 400, + name: 'unauthorized_client', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/unauthorized-request-error.js b/lib/errors/unauthorized-request-error.js index a14af98a..ae4e2dcb 100644 --- a/lib/errors/unauthorized-request-error.js +++ b/lib/errors/unauthorized-request-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * Constructor. @@ -18,15 +18,15 @@ const OAuthError = require("./oauth-error"); */ class UnauthorizedRequestError extends OAuthError { - constructor(message, properties) { - properties = { - code: 401, - name: "unauthorized_request", - ...properties, - }; + constructor(message, properties) { + properties = { + code: 401, + name: 'unauthorized_request', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js index 5745d5d8..961641af 100644 --- a/lib/errors/unsupported-grant-type-error.js +++ b/lib/errors/unsupported-grant-type-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * Constructor. @@ -15,15 +15,15 @@ const OAuthError = require("./oauth-error"); */ class UnsupportedGrantTypeError extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: "unsupported_grant_type", - ...properties, - }; + constructor(message, properties) { + properties = { + code: 400, + name: 'unsupported_grant_type', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/errors/unsupported-response-type-error.js b/lib/errors/unsupported-response-type-error.js index fcedb039..3ecfa596 100644 --- a/lib/errors/unsupported-response-type-error.js +++ b/lib/errors/unsupported-response-type-error.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const OAuthError = require("./oauth-error"); +const OAuthError = require('./oauth-error'); /** * Constructor. @@ -16,15 +16,15 @@ const OAuthError = require("./oauth-error"); */ class UnsupportedResponseTypeError extends OAuthError { - constructor(message, properties) { - properties = { - code: 400, - name: "unsupported_response_type", - ...properties, - }; + constructor(message, properties) { + properties = { + code: 400, + name: 'unsupported_response_type', + ...properties, + }; - super(message, properties); - } + super(message, properties); + } } /** diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js index 80e08bf2..cc2f8683 100644 --- a/lib/grant-types/abstract-grant-type.js +++ b/lib/grant-types/abstract-grant-type.js @@ -1,136 +1,136 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const InvalidScopeError = require("../errors/invalid-scope-error"); -const tokenUtil = require("../utils/token-util"); -const { parseScope } = require("../utils/scope-util"); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const InvalidScopeError = require('../errors/invalid-scope-error'); +const tokenUtil = require('../utils/token-util'); +const { parseScope } = require('../utils/scope-util'); /** * @class * @classDesc# */ class AbstractGrantType { - /** - * @constructor - * @param options {object} - * @param options.accessTokenLifetime {number} access token lifetime in seconds - * @param options.model {Model} the model - * @param options.refreshTokenLifetime {number} - * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. - * @throws {InvalidArgumentError} if {options.accessTokenLifeTime} is missing - * @throws {InvalidArgumentError} if {options.model} is missing - */ - constructor(options) { - options = options || {}; - - if (!options.accessTokenLifetime) { - throw new InvalidArgumentError( - "Missing parameter: `accessTokenLifetime`", - ); - } - - if (!options.model) { - throw new InvalidArgumentError("Missing parameter: `model`"); - } - - this.accessTokenLifetime = options.accessTokenLifetime; - this.model = options.model; - this.refreshTokenLifetime = options.refreshTokenLifetime; - this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken; - } - - /** - * Generate access token. - * If the model implements `generateAccessToken` then - * this implementation will be used. - * Otherwise, falls back to an internal implementation from `TokenUtil.generateRandomToken`. - * - * @param client - * @param user - * @param scope - * @return {Promise} - */ - async generateAccessToken(client, user, scope) { - if (this.model.generateAccessToken) { - // We should not fall back to a random accessToken, if the model did not - // return a token, in order to prevent unintended token-issuing. - return this.model.generateAccessToken(client, user, scope); - } - - return tokenUtil.generateRandomToken(); - } - - /** - * Generate refresh token. - */ - async generateRefreshToken(client, user, scope) { - if (this.model.generateRefreshToken) { - // We should not fall back to a random refreshToken, if the model did not - // return a token, in order to prevent unintended token-issuing. - return this.model.generateRefreshToken(client, user, scope); - } - - return tokenUtil.generateRandomToken(); - } - - /** - * Get access token expiration date. - */ - getAccessTokenExpiresAt() { - return new Date(Date.now() + this.accessTokenLifetime * 1000); - } - - /** - * Get refresh token expiration date (now + refresh token lifetime) - * @returns {Date} - */ - getRefreshTokenExpiresAt() { - return new Date(Date.now() + this.refreshTokenLifetime * 1000); - } - - /** - * Get scope from the request body. - * @param request {Request} - * @returns {string|undefined} - */ - getScope(request) { - return parseScope(request.body.scope); - } - - /** - * Validate requested scope. - * Delegates validation to the Model's `validateScope` method, - * if the model implements this method. - * Otherwise, treats given scope as valid. - * @param user {object} - * @param client {ClientData} - * @param scope {string} - * @return {string} the validated scope - * @throws {InvalidScopeError} if the {Model#validateScope} method returned a falsy value - */ - async validateScope(user, client, scope) { - if (this.model.validateScope) { - const validatedScope = await this.model.validateScope( - user, - client, - scope, - ); - - if (!validatedScope) { - throw new InvalidScopeError( - "Invalid scope: Requested scope is invalid", - ); - } - - return validatedScope; - } else { - return scope; - } - } + /** + * @constructor + * @param options {object} + * @param options.accessTokenLifetime {number} access token lifetime in seconds + * @param options.model {Model} the model + * @param options.refreshTokenLifetime {number} + * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. + * @throws {InvalidArgumentError} if {options.accessTokenLifeTime} is missing + * @throws {InvalidArgumentError} if {options.model} is missing + */ + constructor(options) { + options = options || {}; + + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `accessTokenLifetime`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + this.accessTokenLifetime = options.accessTokenLifetime; + this.model = options.model; + this.refreshTokenLifetime = options.refreshTokenLifetime; + this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken; + } + + /** + * Generate access token. + * If the model implements `generateAccessToken` then + * this implementation will be used. + * Otherwise, falls back to an internal implementation from `TokenUtil.generateRandomToken`. + * + * @param client + * @param user + * @param scope + * @return {Promise} + */ + async generateAccessToken(client, user, scope) { + if (this.model.generateAccessToken) { + // We should not fall back to a random accessToken, if the model did not + // return a token, in order to prevent unintended token-issuing. + return this.model.generateAccessToken(client, user, scope); + } + + return tokenUtil.generateRandomToken(); + } + + /** + * Generate refresh token. + */ + async generateRefreshToken(client, user, scope) { + if (this.model.generateRefreshToken) { + // We should not fall back to a random refreshToken, if the model did not + // return a token, in order to prevent unintended token-issuing. + return this.model.generateRefreshToken(client, user, scope); + } + + return tokenUtil.generateRandomToken(); + } + + /** + * Get access token expiration date. + */ + getAccessTokenExpiresAt() { + return new Date(Date.now() + this.accessTokenLifetime * 1000); + } + + /** + * Get refresh token expiration date (now + refresh token lifetime) + * @returns {Date} + */ + getRefreshTokenExpiresAt() { + return new Date(Date.now() + this.refreshTokenLifetime * 1000); + } + + /** + * Get scope from the request body. + * @param request {Request} + * @returns {string|undefined} + */ + getScope(request) { + return parseScope(request.body.scope); + } + + /** + * Validate requested scope. + * Delegates validation to the Model's `validateScope` method, + * if the model implements this method. + * Otherwise, treats given scope as valid. + * @param user {object} + * @param client {ClientData} + * @param scope {string} + * @return {string} the validated scope + * @throws {InvalidScopeError} if the {Model#validateScope} method returned a falsy value + */ + async validateScope(user, client, scope) { + if (this.model.validateScope) { + const validatedScope = await this.model.validateScope( + user, + client, + scope, + ); + + if (!validatedScope) { + throw new InvalidScopeError( + 'Invalid scope: Requested scope is invalid', + ); + } + + return validatedScope; + } else { + return scope; + } + } } module.exports = AbstractGrantType; diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 3ef8366d..26150f65 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -1,326 +1,326 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const crypto = require("crypto"); -const AbstractGrantType = require("./abstract-grant-type"); -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const InvalidGrantError = require("../errors/invalid-grant-error"); -const InvalidRequestError = require("../errors/invalid-request-error"); -const ServerError = require("../errors/server-error"); -const isFormat = require("@node-oauth/formats"); -const pkce = require("../pkce/pkce"); +const crypto = require('crypto'); +const AbstractGrantType = require('./abstract-grant-type'); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const InvalidGrantError = require('../errors/invalid-grant-error'); +const InvalidRequestError = require('../errors/invalid-request-error'); +const ServerError = require('../errors/server-error'); +const isFormat = require('@node-oauth/formats'); +const pkce = require('../pkce/pkce'); /** * @class * @classDesc */ class AuthorizationCodeGrantType extends AbstractGrantType { - /** - * @constructor - * @param options - */ - constructor(options = {}) { - if (!options.model) { - throw new InvalidArgumentError("Missing parameter: `model`"); - } - - if (!options.model.getAuthorizationCode) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `getAuthorizationCode()`", - ); - } - - if (!options.model.revokeAuthorizationCode) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `revokeAuthorizationCode()`", - ); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `saveToken()`", - ); - } - - super(options); - - // xxx: plain PKCE is only allowed if explicitly enabled - this.enablePlainPKCE = options.enablePlainPKCE === true; - } - - /** - * Handle authorization code grant. - * - * @param request {Request} - * @param client {ClientData} - * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 - */ - - async handle(request, client) { - if (!request) { - throw new InvalidArgumentError("Missing parameter: `request`"); - } - - if (!client) { - throw new InvalidArgumentError("Missing parameter: `client`"); - } - - const code = await this.getAuthorizationCode(request, client); - await this.revokeAuthorizationCode(code); - // xxx: PKCE verification is done after revoking the code, - // so that a failed verification attempt consumes the code and prevents - // online brute-force guessing. - await this.verifyPKCE(request, code); - await this.validateRedirectUri(request, code); - - return this.saveToken( - code.user, - client, - code.authorizationCode, - code.scope, - ); - } - - /** - * Get the authorization code. - * @param request {Request} - * @param client {ClientData} - * @return {Promise<{user}>} - */ - - async getAuthorizationCode(request, client) { - if (!request.body.code) { - throw new InvalidRequestError("Missing parameter: `code`"); - } - - if (!isFormat.vschar(request.body.code)) { - throw new InvalidRequestError("Invalid parameter: `code`"); - } - - const code = await this.model.getAuthorizationCode(request.body.code); - - if (!code) { - throw new InvalidGrantError( - "Invalid grant: authorization code is invalid", - ); - } - - if (!code.client) { - throw new ServerError( - "Server error: `getAuthorizationCode()` did not return a `client` object", - ); - } - - if (!code.user) { - throw new ServerError( - "Server error: `getAuthorizationCode()` did not return a `user` object", - ); - } - - if (code.client.id !== client.id) { - throw new InvalidGrantError( - "Invalid grant: authorization code is invalid", - ); - } - - if (!(code.expiresAt instanceof Date)) { - throw new ServerError( - "Server error: `expiresAt` must be a Date instance", - ); - } - - if (code.expiresAt < new Date()) { - throw new InvalidGrantError( - "Invalid grant: authorization code has expired", - ); - } - - if (code.redirectUri && !isFormat.uri(code.redirectUri)) { - throw new InvalidGrantError( - "Invalid grant: `redirect_uri` is not a valid URI", - ); - } - - return code; - } - - /** - * Verify PKCE code_verifier against the stored code_challenge. - * - * This is called from handle() AFTER the authorization code has been - * revoked, so that a failed verification attempt consumes the code - * and prevents online brute-force guessing. - * - * @param request {Request} - * @param code {AuthorizationCodeData} - * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6 - */ - - verifyPKCE(request, code) { - if (code.codeChallenge) { - const method = this.getCodeChallengeMethod(code.codeChallengeMethod); - - if (!this.enablePlainPKCE && method === "plain") { - throw new InvalidRequestError( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); - } - - if (!request.body.code_verifier) { - throw new InvalidGrantError("Missing parameter: `code_verifier`"); - } - - if (!pkce.codeChallengeMatchesABNF(request.body.code_verifier)) { - throw new InvalidRequestError("Invalid parameter: `code_verifier`"); - } - - const hash = pkce.getHashForCodeChallenge({ - verifier: request.body.code_verifier, - method, - }); - - if (!hash) { - throw new ServerError( - "Server error: no valid hash algorithm available to verify `code_verifier`", - ); - } - - // xxx: Use timingSafeEqual to prevent against timing attacks when comparing - // the hash of the code_verifier to the stored code_challenge. - if (!this.hashesAreEqual(hash, code.codeChallenge)) { - throw new InvalidGrantError("Invalid grant: code verifier is invalid"); - } - } else { - if (request.body.code_verifier) { - // No code challenge but code_verifier was passed in. - throw new InvalidGrantError("Invalid grant: code verifier is invalid"); - } - } - } - - hashesAreEqual(trusted, untrusted) { - const trustedBuf = Buffer.isBuffer(trusted) - ? trusted - : Buffer.from(trusted); - const untrustedBuf = Buffer.isBuffer(untrusted) - ? untrusted - : Buffer.from(untrusted); - const equalLength = trustedBuf.byteLength === untrustedBuf.byteLength; - // if the buffers are the same length, compare them, - // otherwise only compare with the trusted buffer but return false anyway - return ( - crypto.timingSafeEqual( - trustedBuf, - equalLength ? untrustedBuf : trustedBuf, - ) && equalLength - ); - } - - getCodeChallengeMethod(method) { - if (method) { - return method; - } - // Per RFC 7636 §4.6, the default code challenge method is "plain". - // However, plain PKCE is not secure, so we only allow it if explicitly enabled. - return this.enablePlainPKCE ? "plain" : "S256"; - } - - /** - * Validate the redirect URI. - * - * "The authorization server MUST ensure that the redirect_uri parameter is - * present if the redirect_uri parameter was included in the initial - * authorization request as described in Section 4.1.1, and if included - * ensure that their values are identical." - * @param request {Request} - * @param code {AuthorizationCodeData} - * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 - */ - - validateRedirectUri(request, code) { - if (!code.redirectUri) { - return; - } - - const redirectUri = request.body.redirect_uri || request.query.redirect_uri; - - if (!isFormat.uri(redirectUri)) { - throw new InvalidRequestError( - "Invalid request: `redirect_uri` is not a valid URI", - ); - } - - if (redirectUri !== code.redirectUri) { - throw new InvalidRequestError( - "Invalid request: `redirect_uri` is invalid", - ); - } - } - - /** - * Revoke the authorization code. - * - * "The authorization code MUST expire shortly after it is issued to mitigate - * the risk of leaks. [...] If an authorization code is used more than once, - * the authorization server MUST deny the request." - * @param code {AuthorizationCodeData} - * @see https://tools.ietf.org/html/rfc6749#section-4.1.2 - */ - - async revokeAuthorizationCode(code) { - const status = await this.model.revokeAuthorizationCode(code); - - if (!status) { - throw new InvalidGrantError( - "Invalid grant: authorization code is invalid", - ); - } - - return code; - } - - /** - * Save token. - * - * @param user {object} - * @param client {ClientData} - * @param authorizationCode {string} - * @param requestedScope {string} - * - */ - - async saveToken(user, client, authorizationCode, requestedScope) { - const validatedScope = await this.validateScope( - user, - client, - requestedScope, - ); - const accessToken = await this.generateAccessToken( - client, - user, - validatedScope, - ); - const refreshToken = await this.generateRefreshToken( - client, - user, - validatedScope, - ); - const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); - const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); - - const token = { - accessToken, - authorizationCode, - accessTokenExpiresAt, - refreshToken, - refreshTokenExpiresAt, - scope: validatedScope, - }; - - return this.model.saveToken(token, client, user); - } + /** + * @constructor + * @param options + */ + constructor(options = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getAuthorizationCode) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getAuthorizationCode()`', + ); + } + + if (!options.model.revokeAuthorizationCode) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `revokeAuthorizationCode()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + + super(options); + + // xxx: plain PKCE is only allowed if explicitly enabled + this.enablePlainPKCE = options.enablePlainPKCE === true; + } + + /** + * Handle authorization code grant. + * + * @param request {Request} + * @param client {ClientData} + * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 + */ + + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const code = await this.getAuthorizationCode(request, client); + await this.revokeAuthorizationCode(code); + // xxx: PKCE verification is done after revoking the code, + // so that a failed verification attempt consumes the code and prevents + // online brute-force guessing. + await this.verifyPKCE(request, code); + await this.validateRedirectUri(request, code); + + return this.saveToken( + code.user, + client, + code.authorizationCode, + code.scope, + ); + } + + /** + * Get the authorization code. + * @param request {Request} + * @param client {ClientData} + * @return {Promise<{user}>} + */ + + async getAuthorizationCode(request, client) { + if (!request.body.code) { + throw new InvalidRequestError('Missing parameter: `code`'); + } + + if (!isFormat.vschar(request.body.code)) { + throw new InvalidRequestError('Invalid parameter: `code`'); + } + + const code = await this.model.getAuthorizationCode(request.body.code); + + if (!code) { + throw new InvalidGrantError( + 'Invalid grant: authorization code is invalid', + ); + } + + if (!code.client) { + throw new ServerError( + 'Server error: `getAuthorizationCode()` did not return a `client` object', + ); + } + + if (!code.user) { + throw new ServerError( + 'Server error: `getAuthorizationCode()` did not return a `user` object', + ); + } + + if (code.client.id !== client.id) { + throw new InvalidGrantError( + 'Invalid grant: authorization code is invalid', + ); + } + + if (!(code.expiresAt instanceof Date)) { + throw new ServerError( + 'Server error: `expiresAt` must be a Date instance', + ); + } + + if (code.expiresAt < new Date()) { + throw new InvalidGrantError( + 'Invalid grant: authorization code has expired', + ); + } + + if (code.redirectUri && !isFormat.uri(code.redirectUri)) { + throw new InvalidGrantError( + 'Invalid grant: `redirect_uri` is not a valid URI', + ); + } + + return code; + } + + /** + * Verify PKCE code_verifier against the stored code_challenge. + * + * This is called from handle() AFTER the authorization code has been + * revoked, so that a failed verification attempt consumes the code + * and prevents online brute-force guessing. + * + * @param request {Request} + * @param code {AuthorizationCodeData} + * @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6 + */ + + verifyPKCE(request, code) { + if (code.codeChallenge) { + const method = this.getCodeChallengeMethod(code.codeChallengeMethod); + + if (!this.enablePlainPKCE && method === 'plain') { + throw new InvalidRequestError( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } + + if (!request.body.code_verifier) { + throw new InvalidGrantError('Missing parameter: `code_verifier`'); + } + + if (!pkce.codeChallengeMatchesABNF(request.body.code_verifier)) { + throw new InvalidRequestError('Invalid parameter: `code_verifier`'); + } + + const hash = pkce.getHashForCodeChallenge({ + verifier: request.body.code_verifier, + method, + }); + + if (!hash) { + throw new ServerError( + 'Server error: no valid hash algorithm available to verify `code_verifier`', + ); + } + + // xxx: Use timingSafeEqual to prevent against timing attacks when comparing + // the hash of the code_verifier to the stored code_challenge. + if (!this.hashesAreEqual(hash, code.codeChallenge)) { + throw new InvalidGrantError('Invalid grant: code verifier is invalid'); + } + } else { + if (request.body.code_verifier) { + // No code challenge but code_verifier was passed in. + throw new InvalidGrantError('Invalid grant: code verifier is invalid'); + } + } + } + + hashesAreEqual(trusted, untrusted) { + const trustedBuf = Buffer.isBuffer(trusted) + ? trusted + : Buffer.from(trusted); + const untrustedBuf = Buffer.isBuffer(untrusted) + ? untrusted + : Buffer.from(untrusted); + const equalLength = trustedBuf.byteLength === untrustedBuf.byteLength; + // if the buffers are the same length, compare them, + // otherwise only compare with the trusted buffer but return false anyway + return ( + crypto.timingSafeEqual( + trustedBuf, + equalLength ? untrustedBuf : trustedBuf, + ) && equalLength + ); + } + + getCodeChallengeMethod(method) { + if (method) { + return method; + } + // Per RFC 7636 §4.6, the default code challenge method is "plain". + // However, plain PKCE is not secure, so we only allow it if explicitly enabled. + return this.enablePlainPKCE ? 'plain' : 'S256'; + } + + /** + * Validate the redirect URI. + * + * "The authorization server MUST ensure that the redirect_uri parameter is + * present if the redirect_uri parameter was included in the initial + * authorization request as described in Section 4.1.1, and if included + * ensure that their values are identical." + * @param request {Request} + * @param code {AuthorizationCodeData} + * @see https://tools.ietf.org/html/rfc6749#section-4.1.3 + */ + + validateRedirectUri(request, code) { + if (!code.redirectUri) { + return; + } + + const redirectUri = request.body.redirect_uri || request.query.redirect_uri; + + if (!isFormat.uri(redirectUri)) { + throw new InvalidRequestError( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + + if (redirectUri !== code.redirectUri) { + throw new InvalidRequestError( + 'Invalid request: `redirect_uri` is invalid', + ); + } + } + + /** + * Revoke the authorization code. + * + * "The authorization code MUST expire shortly after it is issued to mitigate + * the risk of leaks. [...] If an authorization code is used more than once, + * the authorization server MUST deny the request." + * @param code {AuthorizationCodeData} + * @see https://tools.ietf.org/html/rfc6749#section-4.1.2 + */ + + async revokeAuthorizationCode(code) { + const status = await this.model.revokeAuthorizationCode(code); + + if (!status) { + throw new InvalidGrantError( + 'Invalid grant: authorization code is invalid', + ); + } + + return code; + } + + /** + * Save token. + * + * @param user {object} + * @param client {ClientData} + * @param authorizationCode {string} + * @param requestedScope {string} + * + */ + + async saveToken(user, client, authorizationCode, requestedScope) { + const validatedScope = await this.validateScope( + user, + client, + requestedScope, + ); + const accessToken = await this.generateAccessToken( + client, + user, + validatedScope, + ); + const refreshToken = await this.generateRefreshToken( + client, + user, + validatedScope, + ); + const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); + + const token = { + accessToken, + authorizationCode, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope: validatedScope, + }; + + return this.model.saveToken(token, client, user); + } } /** diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index b06e0a12..4e4d8d64 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -1,103 +1,103 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const AbstractGrantType = require("./abstract-grant-type"); -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const InvalidGrantError = require("../errors/invalid-grant-error"); +const AbstractGrantType = require('./abstract-grant-type'); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const InvalidGrantError = require('../errors/invalid-grant-error'); /** * Constructor. */ class ClientCredentialsGrantType extends AbstractGrantType { - constructor(options = {}) { - if (!options.model) { - throw new InvalidArgumentError("Missing parameter: `model`"); - } - - if (!options.model.getUserFromClient) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `getUserFromClient()`", - ); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `saveToken()`", - ); - } - - super(options); - } - - /** - * Handle client credentials grant. - * - * @see https://tools.ietf.org/html/rfc6749#section-4.4.2 - */ - - async handle(request, client) { - if (!request) { - throw new InvalidArgumentError("Missing parameter: `request`"); - } - - if (!client) { - throw new InvalidArgumentError("Missing parameter: `client`"); - } - - const scope = this.getScope(request); - const user = await this.getUserFromClient(client); - - return this.saveToken(user, client, scope); - } - - /** - * Retrieve the user using client credentials. - */ - - async getUserFromClient(client) { - const user = await this.model.getUserFromClient(client); - - if (!user) { - throw new InvalidGrantError( - "Invalid grant: user credentials are invalid", - ); - } - - return user; - } - - /** - * Save token. - */ - - async saveToken(user, client, requestedScope) { - const validatedScope = await this.validateScope( - user, - client, - requestedScope, - ); - const accessToken = await this.generateAccessToken( - client, - user, - validatedScope, - ); - const accessTokenExpiresAt = await this.getAccessTokenExpiresAt( - client, - user, - validatedScope, - ); - const token = { - accessToken, - accessTokenExpiresAt, - scope: validatedScope, - }; - - return this.model.saveToken(token, client, user); - } + constructor(options = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getUserFromClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getUserFromClient()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + + super(options); + } + + /** + * Handle client credentials grant. + * + * @see https://tools.ietf.org/html/rfc6749#section-4.4.2 + */ + + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const scope = this.getScope(request); + const user = await this.getUserFromClient(client); + + return this.saveToken(user, client, scope); + } + + /** + * Retrieve the user using client credentials. + */ + + async getUserFromClient(client) { + const user = await this.model.getUserFromClient(client); + + if (!user) { + throw new InvalidGrantError( + 'Invalid grant: user credentials are invalid', + ); + } + + return user; + } + + /** + * Save token. + */ + + async saveToken(user, client, requestedScope) { + const validatedScope = await this.validateScope( + user, + client, + requestedScope, + ); + const accessToken = await this.generateAccessToken( + client, + user, + validatedScope, + ); + const accessTokenExpiresAt = await this.getAccessTokenExpiresAt( + client, + user, + validatedScope, + ); + const token = { + accessToken, + accessTokenExpiresAt, + scope: validatedScope, + }; + + return this.model.saveToken(token, client, user); + } } /** diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index 652afd9f..b9da8e79 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -1,14 +1,14 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const AbstractGrantType = require("./abstract-grant-type"); -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const InvalidGrantError = require("../errors/invalid-grant-error"); -const InvalidRequestError = require("../errors/invalid-request-error"); -const isFormat = require("@node-oauth/formats"); +const AbstractGrantType = require('./abstract-grant-type'); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const InvalidGrantError = require('../errors/invalid-grant-error'); +const InvalidRequestError = require('../errors/invalid-request-error'); +const isFormat = require('@node-oauth/formats'); /** * Constructor. @@ -16,116 +16,116 @@ const isFormat = require("@node-oauth/formats"); */ class PasswordGrantType extends AbstractGrantType { - constructor(options = {}) { - if (!options.model) { - throw new InvalidArgumentError("Missing parameter: `model`"); - } - - if (!options.model.getUser) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `getUser()`", - ); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `saveToken()`", - ); - } - - super(options); - } - - /** - * Retrieve the user from the model using a username/password combination. - * - * @see https://tools.ietf.org/html/rfc6749#section-4.3.2 - */ - - async handle(request, client) { - if (!request) { - throw new InvalidArgumentError("Missing parameter: `request`"); - } - - if (!client) { - throw new InvalidArgumentError("Missing parameter: `client`"); - } - - const scope = this.getScope(request); - const user = await this.getUser(request, client); - - return this.saveToken(user, client, scope); - } - - /** - * Get user using a username/password combination. - */ - - async getUser(request, client) { - if (!request.body.username) { - throw new InvalidRequestError("Missing parameter: `username`"); - } - - if (!request.body.password) { - throw new InvalidRequestError("Missing parameter: `password`"); - } - - if (!isFormat.uchar(request.body.username)) { - throw new InvalidRequestError("Invalid parameter: `username`"); - } - - if (!isFormat.uchar(request.body.password)) { - throw new InvalidRequestError("Invalid parameter: `password`"); - } - - const user = await this.model.getUser( - request.body.username, - request.body.password, - client, - ); - - if (!user) { - throw new InvalidGrantError( - "Invalid grant: user credentials are invalid", - ); - } - - return user; - } - - /** - * Save token. - */ - - async saveToken(user, client, requestedScope) { - const validatedScope = await this.validateScope( - user, - client, - requestedScope, - ); - const accessToken = await this.generateAccessToken( - client, - user, - validatedScope, - ); - const refreshToken = await this.generateRefreshToken( - client, - user, - validatedScope, - ); - const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); - const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); - - const token = { - accessToken, - accessTokenExpiresAt, - refreshToken, - refreshTokenExpiresAt, - scope: validatedScope, - }; - - return this.model.saveToken(token, client, user); - } + constructor(options = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getUser) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getUser()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + + super(options); + } + + /** + * Retrieve the user from the model using a username/password combination. + * + * @see https://tools.ietf.org/html/rfc6749#section-4.3.2 + */ + + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + const scope = this.getScope(request); + const user = await this.getUser(request, client); + + return this.saveToken(user, client, scope); + } + + /** + * Get user using a username/password combination. + */ + + async getUser(request, client) { + if (!request.body.username) { + throw new InvalidRequestError('Missing parameter: `username`'); + } + + if (!request.body.password) { + throw new InvalidRequestError('Missing parameter: `password`'); + } + + if (!isFormat.uchar(request.body.username)) { + throw new InvalidRequestError('Invalid parameter: `username`'); + } + + if (!isFormat.uchar(request.body.password)) { + throw new InvalidRequestError('Invalid parameter: `password`'); + } + + const user = await this.model.getUser( + request.body.username, + request.body.password, + client, + ); + + if (!user) { + throw new InvalidGrantError( + 'Invalid grant: user credentials are invalid', + ); + } + + return user; + } + + /** + * Save token. + */ + + async saveToken(user, client, requestedScope) { + const validatedScope = await this.validateScope( + user, + client, + requestedScope, + ); + const accessToken = await this.generateAccessToken( + client, + user, + validatedScope, + ); + const refreshToken = await this.generateRefreshToken( + client, + user, + validatedScope, + ); + const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); + + const token = { + accessToken, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope: validatedScope, + }; + + return this.model.saveToken(token, client, user); + } } /** diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index 451074b4..e6b342cd 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -1,199 +1,199 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const AbstractGrantType = require("./abstract-grant-type"); -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const InvalidGrantError = require("../errors/invalid-grant-error"); -const InvalidRequestError = require("../errors/invalid-request-error"); -const ServerError = require("../errors/server-error"); -const isFormat = require("@node-oauth/formats"); -const InvalidScopeError = require("../errors/invalid-scope-error"); +const AbstractGrantType = require('./abstract-grant-type'); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const InvalidGrantError = require('../errors/invalid-grant-error'); +const InvalidRequestError = require('../errors/invalid-request-error'); +const ServerError = require('../errors/server-error'); +const isFormat = require('@node-oauth/formats'); +const InvalidScopeError = require('../errors/invalid-scope-error'); /** * Constructor. */ class RefreshTokenGrantType extends AbstractGrantType { - constructor(options = {}) { - if (!options.model) { - throw new InvalidArgumentError("Missing parameter: `model`"); - } - - if (!options.model.getRefreshToken) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `getRefreshToken()`", - ); - } - - if (!options.model.revokeToken) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `revokeToken()`", - ); - } - - if (!options.model.saveToken) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `saveToken()`", - ); - } - - super(options); - } - - /** - * Handle refresh token grant. - * - * @see https://tools.ietf.org/html/rfc6749#section-6 - */ - - async handle(request, client) { - if (!request) { - throw new InvalidArgumentError("Missing parameter: `request`"); - } - - if (!client) { - throw new InvalidArgumentError("Missing parameter: `client`"); - } - - let token; - token = await this.getRefreshToken(request, client); - token = await this.revokeToken(token); - - const scope = this.getScope(request, token); - - return this.saveToken(token.user, client, scope); - } - - /** - * Get refresh token. - */ - - async getRefreshToken(request, client) { - if (!request.body.refresh_token) { - throw new InvalidRequestError("Missing parameter: `refresh_token`"); - } - - if (!isFormat.vschar(request.body.refresh_token)) { - throw new InvalidRequestError("Invalid parameter: `refresh_token`"); - } - - const token = await this.model.getRefreshToken(request.body.refresh_token); - - if (!token) { - throw new InvalidGrantError("Invalid grant: refresh token is invalid"); - } - - if (!token.client) { - throw new ServerError( - "Server error: `getRefreshToken()` did not return a `client` object", - ); - } - - if (!token.user) { - throw new ServerError( - "Server error: `getRefreshToken()` did not return a `user` object", - ); - } - - if (token.client.id !== client.id) { - throw new InvalidGrantError( - "Invalid grant: refresh token was issued to another client", - ); - } - - if ( - token.refreshTokenExpiresAt && - !(token.refreshTokenExpiresAt instanceof Date) - ) { - throw new ServerError( - "Server error: `refreshTokenExpiresAt` must be a Date instance", - ); - } - - if ( - token.refreshTokenExpiresAt && - token.refreshTokenExpiresAt < new Date() - ) { - throw new InvalidGrantError("Invalid grant: refresh token has expired"); - } - - return token; - } - - /** - * Revoke the refresh token. - * - * @see https://tools.ietf.org/html/rfc6749#section-6 - */ - - async revokeToken(token) { - if (this.alwaysIssueNewRefreshToken === false) { - return token; - } - - const status = await this.model.revokeToken(token); - - if (!status) { - throw new InvalidGrantError( - "Invalid grant: refresh token is invalid or could not be revoked", - ); - } - - return token; - } - - /** - * Save token. - */ - - async saveToken(user, client, scope) { - const accessToken = await this.generateAccessToken(client, user, scope); - const refreshToken = await this.generateRefreshToken(client, user, scope); - const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); - const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); - const token = { - accessToken, - accessTokenExpiresAt, - scope, - }; - - if (this.alwaysIssueNewRefreshToken !== false) { - token.refreshToken = refreshToken; - token.refreshTokenExpiresAt = refreshTokenExpiresAt; - } - - return this.model.saveToken(token, client, user); - } - - getScope(request, token) { - const requestedScope = super.getScope(request); - const originalScope = token.scope; - - if (!originalScope && !requestedScope) { - return; - } - - if (!originalScope && requestedScope) { - throw new InvalidScopeError("Invalid scope: Unable to add extra scopes"); - } - - if (!requestedScope) { - return originalScope; - } - - const valid = requestedScope.every((scope) => { - return originalScope.includes(scope); - }); - - if (!valid) { - throw new InvalidScopeError("Invalid scope: Unable to add extra scopes"); - } - - return requestedScope; - } + constructor(options = {}) { + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getRefreshToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getRefreshToken()`', + ); + } + + if (!options.model.revokeToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `revokeToken()`', + ); + } + + if (!options.model.saveToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + + super(options); + } + + /** + * Handle refresh token grant. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + + async handle(request, client) { + if (!request) { + throw new InvalidArgumentError('Missing parameter: `request`'); + } + + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } + + let token; + token = await this.getRefreshToken(request, client); + token = await this.revokeToken(token); + + const scope = this.getScope(request, token); + + return this.saveToken(token.user, client, scope); + } + + /** + * Get refresh token. + */ + + async getRefreshToken(request, client) { + if (!request.body.refresh_token) { + throw new InvalidRequestError('Missing parameter: `refresh_token`'); + } + + if (!isFormat.vschar(request.body.refresh_token)) { + throw new InvalidRequestError('Invalid parameter: `refresh_token`'); + } + + const token = await this.model.getRefreshToken(request.body.refresh_token); + + if (!token) { + throw new InvalidGrantError('Invalid grant: refresh token is invalid'); + } + + if (!token.client) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `client` object', + ); + } + + if (!token.user) { + throw new ServerError( + 'Server error: `getRefreshToken()` did not return a `user` object', + ); + } + + if (token.client.id !== client.id) { + throw new InvalidGrantError( + 'Invalid grant: refresh token was issued to another client', + ); + } + + if ( + token.refreshTokenExpiresAt && + !(token.refreshTokenExpiresAt instanceof Date) + ) { + throw new ServerError( + 'Server error: `refreshTokenExpiresAt` must be a Date instance', + ); + } + + if ( + token.refreshTokenExpiresAt && + token.refreshTokenExpiresAt < new Date() + ) { + throw new InvalidGrantError('Invalid grant: refresh token has expired'); + } + + return token; + } + + /** + * Revoke the refresh token. + * + * @see https://tools.ietf.org/html/rfc6749#section-6 + */ + + async revokeToken(token) { + if (this.alwaysIssueNewRefreshToken === false) { + return token; + } + + const status = await this.model.revokeToken(token); + + if (!status) { + throw new InvalidGrantError( + 'Invalid grant: refresh token is invalid or could not be revoked', + ); + } + + return token; + } + + /** + * Save token. + */ + + async saveToken(user, client, scope) { + const accessToken = await this.generateAccessToken(client, user, scope); + const refreshToken = await this.generateRefreshToken(client, user, scope); + const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); + const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); + const token = { + accessToken, + accessTokenExpiresAt, + scope, + }; + + if (this.alwaysIssueNewRefreshToken !== false) { + token.refreshToken = refreshToken; + token.refreshTokenExpiresAt = refreshTokenExpiresAt; + } + + return this.model.saveToken(token, client, user); + } + + getScope(request, token) { + const requestedScope = super.getScope(request); + const originalScope = token.scope; + + if (!originalScope && !requestedScope) { + return; + } + + if (!originalScope && requestedScope) { + throw new InvalidScopeError('Invalid scope: Unable to add extra scopes'); + } + + if (!requestedScope) { + return originalScope; + } + + const valid = requestedScope.every((scope) => { + return originalScope.includes(scope); + }); + + if (!valid) { + throw new InvalidScopeError('Invalid scope: Unable to add extra scopes'); + } + + return requestedScope; + } } /** diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 979c7912..3dd9f291 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -1,317 +1,317 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const InvalidRequestError = require("../errors/invalid-request-error"); -const InsufficientScopeError = require("../errors/insufficient-scope-error"); -const InvalidTokenError = require("../errors/invalid-token-error"); -const OAuthError = require("../errors/oauth-error"); -const Request = require("../request"); -const Response = require("../response"); -const ServerError = require("../errors/server-error"); -const UnauthorizedRequestError = require("../errors/unauthorized-request-error"); -const { parseScope } = require("../utils/scope-util"); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const InvalidRequestError = require('../errors/invalid-request-error'); +const InsufficientScopeError = require('../errors/insufficient-scope-error'); +const InvalidTokenError = require('../errors/invalid-token-error'); +const OAuthError = require('../errors/oauth-error'); +const Request = require('../request'); +const Response = require('../response'); +const ServerError = require('../errors/server-error'); +const UnauthorizedRequestError = require('../errors/unauthorized-request-error'); +const { parseScope } = require('../utils/scope-util'); /** * @class * @classDesc */ class AuthenticateHandler { - /** - * @constructor - * @param options {object} Server options. - * @param options.model {Model} The Model; this is always required. - * @param options.scope {string[]|undefined} The scope(s) to authenticate. - * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. - * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. - * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. - * @throws {InvalidArgumentError} if {model} is missing or does not implement `getAccessToken` - */ - constructor(options) { - options = options || {}; - - if (!options.model) { - throw new InvalidArgumentError("Missing parameter: `model`"); - } - - if (!options.model.getAccessToken) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `getAccessToken()`", - ); - } - - if (options.scope && undefined === options.addAcceptedScopesHeader) { - throw new InvalidArgumentError( - "Missing parameter: `addAcceptedScopesHeader`", - ); - } - - if (options.scope && undefined === options.addAuthorizedScopesHeader) { - throw new InvalidArgumentError( - "Missing parameter: `addAuthorizedScopesHeader`", - ); - } - - if (options.scope && !options.model.verifyScope) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `verifyScope()`", - ); - } - - this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; - this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; - this.allowBearerTokensInQueryString = - options.allowBearerTokensInQueryString; - this.model = options.model; - this.scope = Array.isArray(options.scope) - ? options.scope - : parseScope(options.scope); - } - - /** - * Handles the authentication - * @param request {Request} - * @param response {Response} - * @return {Promise<*>} - */ - async handle(request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError( - "Invalid argument: `request` must be an instance of Request", - ); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError( - "Invalid argument: `response` must be an instance of Response", - ); - } - - try { - const requestToken = await this.getTokenFromRequest(request); - - let accessToken; - accessToken = await this.getAccessToken(requestToken); - accessToken = await this.validateAccessToken(accessToken); - - if (this.scope) { - await this.verifyScope(accessToken); - } - - this.updateResponse(response, accessToken); - - return accessToken; - } catch (e) { - // Include the "WWW-Authenticate" response header field if the client - // lacks any authentication information. - // - // @see https://tools.ietf.org/html/rfc6750#section-3.1 - if (e instanceof UnauthorizedRequestError) { - response.set("WWW-Authenticate", 'Bearer realm="Service"'); - } else if (e instanceof InvalidRequestError) { - response.set( - "WWW-Authenticate", - 'Bearer realm="Service",error="invalid_request"', - ); - } else if (e instanceof InvalidTokenError) { - response.set( - "WWW-Authenticate", - 'Bearer realm="Service",error="invalid_token"', - ); - } else if (e instanceof InsufficientScopeError) { - response.set( - "WWW-Authenticate", - 'Bearer realm="Service",error="insufficient_scope"', - ); - } - - if (!(e instanceof OAuthError)) { - throw new ServerError(e); - } - - throw e; - } - } - - /** - * Get the token from the header or body, depending on the request. - * - * "Clients MUST NOT use more than one method to transmit the token in each request." - * - * @param {Request} - * @see {https://tools.ietf.org/html/rfc6750#section-2} - */ - getTokenFromRequest(request) { - const headerToken = request.get("Authorization"); - const queryToken = request.query.access_token; - const bodyToken = request.body.access_token; - - if (!!headerToken + !!queryToken + !!bodyToken > 1) { - throw new InvalidRequestError( - "Invalid request: only one authentication method is allowed", - ); - } - - if (headerToken) { - return this.getTokenFromRequestHeader(request); - } - - if (queryToken) { - return this.getTokenFromRequestQuery(request); - } - - if (bodyToken) { - return this.getTokenFromRequestBody(request); - } - - throw new UnauthorizedRequestError( - "Unauthorized request: no authentication given", - ); - } - - /** - * Get the token from the request header. - * - * @param request {Request} - * @see {http://tools.ietf.org/html/rfc6750#section-2.1} - */ - - getTokenFromRequestHeader(request) { - const token = request.get("Authorization"); - const matches = token.match(/^Bearer ([0-9a-zA-Z-._~+/]+=*)$/); - - if (!matches) { - throw new InvalidRequestError( - "Invalid request: malformed authorization header", - ); - } - - return matches[1]; - } - - /** - * Get the token from the request query. - * - * "Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page - * URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be - * passed in HTTP message headers or message bodies for which confidentiality measures - * are taken. Browsers, web servers, and other software may not adequately secure URLs - * in the browser history, web server logs, and other data structures. If bearer tokens - * are passed in page URLs, attackers might be able to steal them from the history data, - * logs, or other unsecured locations." - * - * @param request {Request} - * @see http://tools.ietf.org/html/rfc6750#section-2.3 - */ - - getTokenFromRequestQuery(request) { - if (!this.allowBearerTokensInQueryString) { - throw new InvalidRequestError( - "Invalid request: do not send bearer tokens in query URLs", - ); - } - - return request.query.access_token; - } - - /** - * Get the token from the request body. - * - * "The HTTP request method is one for which the request-body has defined semantics. - * In particular, this means that the "GET" method MUST NOT be used." - * @param request {Request} - * @see http://tools.ietf.org/html/rfc6750#section-2.2 - */ - - getTokenFromRequestBody(request) { - if (request.method === "GET") { - throw new InvalidRequestError( - "Invalid request: token may not be passed in the body when using the GET verb", - ); - } - - if (!request.is("application/x-www-form-urlencoded")) { - throw new InvalidRequestError( - "Invalid request: content must be application/x-www-form-urlencoded", - ); - } - - return request.body.access_token; - } - - /** - * Get the access token from the model. - * @param token - */ - - async getAccessToken(token) { - const accessToken = await this.model.getAccessToken(token); - - if (!accessToken) { - throw new InvalidTokenError("Invalid token: access token is invalid"); - } - - if (!accessToken.user) { - throw new ServerError( - "Server error: `getAccessToken()` did not return a `user` object", - ); - } - - return accessToken; - } - - /** - * Validate access token. - */ - - validateAccessToken(accessToken) { - if (!(accessToken.accessTokenExpiresAt instanceof Date)) { - throw new ServerError( - "Server error: `accessTokenExpiresAt` must be a Date instance", - ); - } - - if (accessToken.accessTokenExpiresAt < new Date()) { - throw new InvalidTokenError("Invalid token: access token has expired"); - } - - return accessToken; - } - - /** - * Verify scope. - */ - - async verifyScope(accessToken) { - const scope = await this.model.verifyScope(accessToken, this.scope); - - if (!scope) { - throw new InsufficientScopeError( - "Insufficient scope: authorized scope is insufficient", - ); - } - } - - /** - * Update response. - */ - - updateResponse(response, accessToken) { - if (accessToken.scope == null) { - return; - } - - if (this.scope && this.addAcceptedScopesHeader) { - response.set("X-Accepted-OAuth-Scopes", this.scope.join(" ")); - } - - if (this.scope && this.addAuthorizedScopesHeader) { - response.set("X-OAuth-Scopes", accessToken.scope.join(" ")); - } - } + /** + * @constructor + * @param options {object} Server options. + * @param options.model {Model} The Model; this is always required. + * @param options.scope {string[]|undefined} The scope(s) to authenticate. + * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. + * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. + * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. + * @throws {InvalidArgumentError} if {model} is missing or does not implement `getAccessToken` + */ + constructor(options) { + options = options || {}; + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getAccessToken) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + + if (options.scope && undefined === options.addAcceptedScopesHeader) { + throw new InvalidArgumentError( + 'Missing parameter: `addAcceptedScopesHeader`', + ); + } + + if (options.scope && undefined === options.addAuthorizedScopesHeader) { + throw new InvalidArgumentError( + 'Missing parameter: `addAuthorizedScopesHeader`', + ); + } + + if (options.scope && !options.model.verifyScope) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `verifyScope()`', + ); + } + + this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; + this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; + this.allowBearerTokensInQueryString = + options.allowBearerTokensInQueryString; + this.model = options.model; + this.scope = Array.isArray(options.scope) + ? options.scope + : parseScope(options.scope); + } + + /** + * Handles the authentication + * @param request {Request} + * @param response {Response} + * @return {Promise<*>} + */ + async handle(request, response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + try { + const requestToken = await this.getTokenFromRequest(request); + + let accessToken; + accessToken = await this.getAccessToken(requestToken); + accessToken = await this.validateAccessToken(accessToken); + + if (this.scope) { + await this.verifyScope(accessToken); + } + + this.updateResponse(response, accessToken); + + return accessToken; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // lacks any authentication information. + // + // @see https://tools.ietf.org/html/rfc6750#section-3.1 + if (e instanceof UnauthorizedRequestError) { + response.set('WWW-Authenticate', 'Bearer realm="Service"'); + } else if (e instanceof InvalidRequestError) { + response.set( + 'WWW-Authenticate', + 'Bearer realm="Service",error="invalid_request"', + ); + } else if (e instanceof InvalidTokenError) { + response.set( + 'WWW-Authenticate', + 'Bearer realm="Service",error="invalid_token"', + ); + } else if (e instanceof InsufficientScopeError) { + response.set( + 'WWW-Authenticate', + 'Bearer realm="Service",error="insufficient_scope"', + ); + } + + if (!(e instanceof OAuthError)) { + throw new ServerError(e); + } + + throw e; + } + } + + /** + * Get the token from the header or body, depending on the request. + * + * "Clients MUST NOT use more than one method to transmit the token in each request." + * + * @param {Request} + * @see {https://tools.ietf.org/html/rfc6750#section-2} + */ + getTokenFromRequest(request) { + const headerToken = request.get('Authorization'); + const queryToken = request.query.access_token; + const bodyToken = request.body.access_token; + + if (!!headerToken + !!queryToken + !!bodyToken > 1) { + throw new InvalidRequestError( + 'Invalid request: only one authentication method is allowed', + ); + } + + if (headerToken) { + return this.getTokenFromRequestHeader(request); + } + + if (queryToken) { + return this.getTokenFromRequestQuery(request); + } + + if (bodyToken) { + return this.getTokenFromRequestBody(request); + } + + throw new UnauthorizedRequestError( + 'Unauthorized request: no authentication given', + ); + } + + /** + * Get the token from the request header. + * + * @param request {Request} + * @see {http://tools.ietf.org/html/rfc6750#section-2.1} + */ + + getTokenFromRequestHeader(request) { + const token = request.get('Authorization'); + const matches = token.match(/^Bearer ([0-9a-zA-Z-._~+/]+=*)$/); + + if (!matches) { + throw new InvalidRequestError( + 'Invalid request: malformed authorization header', + ); + } + + return matches[1]; + } + + /** + * Get the token from the request query. + * + * "Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page + * URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be + * passed in HTTP message headers or message bodies for which confidentiality measures + * are taken. Browsers, web servers, and other software may not adequately secure URLs + * in the browser history, web server logs, and other data structures. If bearer tokens + * are passed in page URLs, attackers might be able to steal them from the history data, + * logs, or other unsecured locations." + * + * @param request {Request} + * @see http://tools.ietf.org/html/rfc6750#section-2.3 + */ + + getTokenFromRequestQuery(request) { + if (!this.allowBearerTokensInQueryString) { + throw new InvalidRequestError( + 'Invalid request: do not send bearer tokens in query URLs', + ); + } + + return request.query.access_token; + } + + /** + * Get the token from the request body. + * + * "The HTTP request method is one for which the request-body has defined semantics. + * In particular, this means that the "GET" method MUST NOT be used." + * @param request {Request} + * @see http://tools.ietf.org/html/rfc6750#section-2.2 + */ + + getTokenFromRequestBody(request) { + if (request.method === 'GET') { + throw new InvalidRequestError( + 'Invalid request: token may not be passed in the body when using the GET verb', + ); + } + + if (!request.is('application/x-www-form-urlencoded')) { + throw new InvalidRequestError( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + + return request.body.access_token; + } + + /** + * Get the access token from the model. + * @param token + */ + + async getAccessToken(token) { + const accessToken = await this.model.getAccessToken(token); + + if (!accessToken) { + throw new InvalidTokenError('Invalid token: access token is invalid'); + } + + if (!accessToken.user) { + throw new ServerError( + 'Server error: `getAccessToken()` did not return a `user` object', + ); + } + + return accessToken; + } + + /** + * Validate access token. + */ + + validateAccessToken(accessToken) { + if (!(accessToken.accessTokenExpiresAt instanceof Date)) { + throw new ServerError( + 'Server error: `accessTokenExpiresAt` must be a Date instance', + ); + } + + if (accessToken.accessTokenExpiresAt < new Date()) { + throw new InvalidTokenError('Invalid token: access token has expired'); + } + + return accessToken; + } + + /** + * Verify scope. + */ + + async verifyScope(accessToken) { + const scope = await this.model.verifyScope(accessToken, this.scope); + + if (!scope) { + throw new InsufficientScopeError( + 'Insufficient scope: authorized scope is insufficient', + ); + } + } + + /** + * Update response. + */ + + updateResponse(response, accessToken) { + if (accessToken.scope == null) { + return; + } + + if (this.scope && this.addAcceptedScopesHeader) { + response.set('X-Accepted-OAuth-Scopes', this.scope.join(' ')); + } + + if (this.scope && this.addAuthorizedScopesHeader) { + response.set('X-OAuth-Scopes', accessToken.scope.join(' ')); + } + } } module.exports = AuthenticateHandler; diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index af2115fe..463ba799 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -1,34 +1,34 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const AccessDeniedError = require("../errors/access-denied-error"); -const AuthenticateHandler = require("../handlers/authenticate-handler"); -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const InvalidClientError = require("../errors/invalid-client-error"); -const InvalidRequestError = require("../errors/invalid-request-error"); -const InvalidScopeError = require("../errors/invalid-scope-error"); -const UnsupportedResponseTypeError = require("../errors/unsupported-response-type-error"); -const OAuthError = require("../errors/oauth-error"); -const Request = require("../request"); -const Response = require("../response"); -const ServerError = require("../errors/server-error"); -const UnauthorizedClientError = require("../errors/unauthorized-client-error"); -const isFormat = require("@node-oauth/formats"); -const tokenUtil = require("../utils/token-util"); -const url = require("url"); -const pkce = require("../pkce/pkce"); -const { parseScope } = require("../utils/scope-util"); +const AccessDeniedError = require('../errors/access-denied-error'); +const AuthenticateHandler = require('../handlers/authenticate-handler'); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const InvalidClientError = require('../errors/invalid-client-error'); +const InvalidRequestError = require('../errors/invalid-request-error'); +const InvalidScopeError = require('../errors/invalid-scope-error'); +const UnsupportedResponseTypeError = require('../errors/unsupported-response-type-error'); +const OAuthError = require('../errors/oauth-error'); +const Request = require('../request'); +const Response = require('../response'); +const ServerError = require('../errors/server-error'); +const UnauthorizedClientError = require('../errors/unauthorized-client-error'); +const isFormat = require('@node-oauth/formats'); +const tokenUtil = require('../utils/token-util'); +const url = require('url'); +const pkce = require('../pkce/pkce'); +const { parseScope } = require('../utils/scope-util'); /** * Response types. */ const responseTypes = { - code: require("../response-types/code-response-type"), - //token: require('../response-types/token-response-type') + code: require('../response-types/code-response-type'), + //token: require('../response-types/token-response-type') }; /** @@ -36,441 +36,441 @@ const responseTypes = { */ class AuthorizeHandler { - constructor(options) { - options = options || {}; - - if (options.authenticateHandler && !options.authenticateHandler.handle) { - throw new InvalidArgumentError( - "Invalid argument: authenticateHandler does not implement `handle()`", - ); - } - - if (!options.authorizationCodeLifetime) { - throw new InvalidArgumentError( - "Missing parameter: `authorizationCodeLifetime`", - ); - } - - if (!options.model) { - throw new InvalidArgumentError("Missing parameter: `model`"); - } - - if (!options.model.getClient) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `getClient()`", - ); - } - - if (!options.model.saveAuthorizationCode) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `saveAuthorizationCode()`", - ); - } - - this.allowEmptyState = options.allowEmptyState; - this.authenticateHandler = - options.authenticateHandler || new AuthenticateHandler(options); - this.authorizationCodeLifetime = options.authorizationCodeLifetime; - this.enablePlainPKCE = options.enablePlainPKCE === true; - this.model = options.model; - } - - /** - * Authorize Handler. - */ - - async handle(request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError( - "Invalid argument: `request` must be an instance of Request", - ); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError( - "Invalid argument: `response` must be an instance of Response", - ); - } - - const expiresAt = await this.getAuthorizationCodeLifetime(); - const client = await this.getClient(request); - const user = await this.getUser(request, response); - - let uri; - let state; - - try { - uri = this.getRedirectUri(request, client); - state = this.getState(request); - - if ( - request.query.allowed === "false" || - request.body.allowed === "false" - ) { - throw new AccessDeniedError( - "Access denied: user denied access to application", - ); - } - - const requestedScope = await this.getScope(request); - const validScope = await this.validateScope(user, client, requestedScope); - const authorizationCode = await this.generateAuthorizationCode( - client, - user, - validScope, - ); - - const ResponseType = this.getResponseType(request); - const codeChallenge = this.getCodeChallenge(request); - const codeChallengeMethod = this.getCodeChallengeMethod(request); - const code = await this.saveAuthorizationCode( - authorizationCode, - expiresAt, - validScope, - client, - uri, - user, - codeChallenge, - codeChallengeMethod, - ); - - const responseTypeInstance = new ResponseType(code.authorizationCode); - const redirectUri = this.buildSuccessRedirectUri( - uri, - responseTypeInstance, - ); - - this.updateResponse(response, redirectUri, state); - - return code; - } catch (err) { - let e = err; - - if (!(e instanceof OAuthError)) { - e = new ServerError(e); - } - const redirectUri = this.buildErrorRedirectUri(uri, e); - this.updateResponse(response, redirectUri, state); - - throw e; - } - } - - /** - * Generate authorization code. - */ - - async generateAuthorizationCode(client, user, scope) { - if (this.model.generateAuthorizationCode) { - return this.model.generateAuthorizationCode(client, user, scope); - } - return tokenUtil.generateRandomToken(); - } - - /** - * Get authorization code lifetime. - */ - - getAuthorizationCodeLifetime() { - const expires = new Date(); - - expires.setSeconds(expires.getSeconds() + this.authorizationCodeLifetime); - return expires; - } - - /** - * Get the client from the model. - */ - - async getClient(request) { - const self = this; - const clientId = request.body.client_id || request.query.client_id; - - if (!clientId) { - throw new InvalidRequestError("Missing parameter: `client_id`"); - } - - if (!isFormat.vschar(clientId)) { - throw new InvalidRequestError("Invalid parameter: `client_id`"); - } - - const redirectUri = request.body.redirect_uri || request.query.redirect_uri; - - if (redirectUri && !isFormat.uri(redirectUri)) { - throw new InvalidRequestError( - "Invalid request: `redirect_uri` is not a valid URI", - ); - } - - const client = await this.model.getClient(clientId, null); - - if (!client) { - throw new InvalidClientError( - "Invalid client: client credentials are invalid", - ); - } - - if (!client.grants) { - throw new InvalidClientError("Invalid client: missing client `grants`"); - } - - if ( - !Array.isArray(client.grants) || - !client.grants.includes("authorization_code") - ) { - throw new UnauthorizedClientError( - "Unauthorized client: `grant_type` is invalid", - ); - } - - if (!client.redirectUris || 0 === client.redirectUris.length) { - throw new InvalidClientError( - "Invalid client: missing client `redirectUri`", - ); - } - - if (redirectUri) { - const valid = await self.validateRedirectUri(redirectUri, client); - - if (!valid) { - throw new InvalidClientError( - "Invalid client: `redirect_uri` does not match client value", - ); - } - } - - return client; - } - - /** - * Validate requested scope. - */ - async validateScope(user, client, scope) { - if (this.model.validateScope) { - const validatedScope = await this.model.validateScope( - user, - client, - scope, - ); - - if (!validatedScope) { - throw new InvalidScopeError( - "Invalid scope: Requested scope is invalid", - ); - } - - return validatedScope; - } - - return scope; - } - - /** - * Get scope from the request. - */ - - getScope(request) { - const scope = request.body.scope || request.query.scope; - - return parseScope(scope); - } - - /** - * Get state from the request. - */ - - getState(request) { - const state = request.body.state || request.query.state; - const stateExists = state && state.length > 0; - const stateIsValid = stateExists - ? isFormat.vschar(state) - : this.allowEmptyState; - - if (!stateIsValid) { - const message = !stateExists ? "Missing" : "Invalid"; - throw new InvalidRequestError(`${message} parameter: \`state\``); - } - - return state; - } - - /** - * Get user by calling the authenticate middleware. - */ - - async getUser(request, response) { - if (this.authenticateHandler instanceof AuthenticateHandler) { - const handled = await this.authenticateHandler.handle(request, response); - return handled ? handled.user : undefined; - } - - const user = await this.authenticateHandler.handle(request, response); - - if (!user) { - throw new ServerError( - "Server error: `handle()` did not return a `user` object", - ); - } - - return user; - } - - /** - * Get redirect URI. - */ - - getRedirectUri(request, client) { - return ( - request.body.redirect_uri || - request.query.redirect_uri || - client.redirectUris[0] - ); - } - - /** - * Save authorization code. - */ - - async saveAuthorizationCode( - authorizationCode, - expiresAt, - scope, - client, - redirectUri, - user, - codeChallenge, - codeChallengeMethod, - ) { - let code = { - authorizationCode: authorizationCode, - expiresAt: expiresAt, - redirectUri: redirectUri, - scope: scope, - }; - - if (codeChallenge && codeChallengeMethod) { - code = Object.assign( - { - codeChallenge: codeChallenge, - codeChallengeMethod: codeChallengeMethod, - }, - code, - ); - } - - return this.model.saveAuthorizationCode(code, client, user); - } - - async validateRedirectUri(redirectUri, client) { - if (this.model.validateRedirectUri) { - return this.model.validateRedirectUri(redirectUri, client); - } - - return client.redirectUris.includes(redirectUri); - } - /** - * Get response type. - */ - - getResponseType(request) { - const responseType = - request.body.response_type || request.query.response_type; - - if (!responseType) { - throw new InvalidRequestError("Missing parameter: `response_type`"); - } - - if (!Object.prototype.hasOwnProperty.call(responseTypes, responseType)) { - throw new UnsupportedResponseTypeError( - "Unsupported response type: `response_type` is not supported", - ); - } - - return responseTypes[responseType]; - } - - /** - * Build a successful response that redirects the user-agent to the client-provided url. - */ - - buildSuccessRedirectUri(redirectUri, responseType) { - return responseType.buildRedirectUri(redirectUri); - } - - /** - * Build an error response that redirects the user-agent to the client-provided url. - */ - - buildErrorRedirectUri(redirectUri, error) { - const uri = url.parse(redirectUri); - - uri.query = { - error: error.name, - }; - - if (error.message) { - uri.query.error_description = error.message; - } - - return uri; - } - - /** - * Update response with the redirect uri and the state parameter, if available. - */ - - updateResponse(response, redirectUri, state) { - redirectUri.query = redirectUri.query || {}; - - if (state) { - redirectUri.query.state = state; - } - - response.redirect(url.format(redirectUri)); - } - - getCodeChallenge(request) { - return request.body.code_challenge || request.query.code_challenge; - } - - /** - * Get code challenge method from request. - * - * When `enablePlainPKCE` is false (the default), the "plain" method is - * rejected and the default (when no method is provided) is "S256". - * When `enablePlainPKCE` is true, "plain" is accepted and used as the - * default per RFC 7636 §4.3. - * - * @see https://www.rfc-editor.org/rfc/rfc7636#section-4.3 - * @throws {InvalidRequestError} if request contains unsupported code_challenge_method - * (see https://www.rfc-editor.org/rfc/rfc7636#section-4.4) - */ - getCodeChallengeMethod(request) { - const algorithm = - request.body.code_challenge_method || request.query.code_challenge_method; - - if (algorithm && !pkce.isValidMethod(algorithm)) { - throw new InvalidRequestError( - `Invalid request: transform algorithm '${algorithm}' not supported`, - ); - } - - if (!this.enablePlainPKCE && algorithm === "plain") { - throw new InvalidRequestError( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); - } - - // return the verified algorithm, if provided - if (algorithm) { - return algorithm; - } - - // otherwise, return the default algorithm based on the value of `enablePlainPKCE` - // which enables extended hardening by default, while - // optionally enable legacy support for the "plain" method per RFC 7636 §4.3 - return this.enablePlainPKCE ? "plain" : "S256"; - } + constructor(options) { + options = options || {}; + + if (options.authenticateHandler && !options.authenticateHandler.handle) { + throw new InvalidArgumentError( + 'Invalid argument: authenticateHandler does not implement `handle()`', + ); + } + + if (!options.authorizationCodeLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `authorizationCodeLifetime`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getClient()`', + ); + } + + if (!options.model.saveAuthorizationCode) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `saveAuthorizationCode()`', + ); + } + + this.allowEmptyState = options.allowEmptyState; + this.authenticateHandler = + options.authenticateHandler || new AuthenticateHandler(options); + this.authorizationCodeLifetime = options.authorizationCodeLifetime; + this.enablePlainPKCE = options.enablePlainPKCE === true; + this.model = options.model; + } + + /** + * Authorize Handler. + */ + + async handle(request, response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + const expiresAt = await this.getAuthorizationCodeLifetime(); + const client = await this.getClient(request); + const user = await this.getUser(request, response); + + let uri; + let state; + + try { + uri = this.getRedirectUri(request, client); + state = this.getState(request); + + if ( + request.query.allowed === 'false' || + request.body.allowed === 'false' + ) { + throw new AccessDeniedError( + 'Access denied: user denied access to application', + ); + } + + const requestedScope = await this.getScope(request); + const validScope = await this.validateScope(user, client, requestedScope); + const authorizationCode = await this.generateAuthorizationCode( + client, + user, + validScope, + ); + + const ResponseType = this.getResponseType(request); + const codeChallenge = this.getCodeChallenge(request); + const codeChallengeMethod = this.getCodeChallengeMethod(request); + const code = await this.saveAuthorizationCode( + authorizationCode, + expiresAt, + validScope, + client, + uri, + user, + codeChallenge, + codeChallengeMethod, + ); + + const responseTypeInstance = new ResponseType(code.authorizationCode); + const redirectUri = this.buildSuccessRedirectUri( + uri, + responseTypeInstance, + ); + + this.updateResponse(response, redirectUri, state); + + return code; + } catch (err) { + let e = err; + + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + const redirectUri = this.buildErrorRedirectUri(uri, e); + this.updateResponse(response, redirectUri, state); + + throw e; + } + } + + /** + * Generate authorization code. + */ + + async generateAuthorizationCode(client, user, scope) { + if (this.model.generateAuthorizationCode) { + return this.model.generateAuthorizationCode(client, user, scope); + } + return tokenUtil.generateRandomToken(); + } + + /** + * Get authorization code lifetime. + */ + + getAuthorizationCodeLifetime() { + const expires = new Date(); + + expires.setSeconds(expires.getSeconds() + this.authorizationCodeLifetime); + return expires; + } + + /** + * Get the client from the model. + */ + + async getClient(request) { + const self = this; + const clientId = request.body.client_id || request.query.client_id; + + if (!clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + if (!isFormat.vschar(clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + + const redirectUri = request.body.redirect_uri || request.query.redirect_uri; + + if (redirectUri && !isFormat.uri(redirectUri)) { + throw new InvalidRequestError( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + + const client = await this.model.getClient(clientId, null); + + if (!client) { + throw new InvalidClientError( + 'Invalid client: client credentials are invalid', + ); + } + + if (!client.grants) { + throw new InvalidClientError('Invalid client: missing client `grants`'); + } + + if ( + !Array.isArray(client.grants) || + !client.grants.includes('authorization_code') + ) { + throw new UnauthorizedClientError( + 'Unauthorized client: `grant_type` is invalid', + ); + } + + if (!client.redirectUris || 0 === client.redirectUris.length) { + throw new InvalidClientError( + 'Invalid client: missing client `redirectUri`', + ); + } + + if (redirectUri) { + const valid = await self.validateRedirectUri(redirectUri, client); + + if (!valid) { + throw new InvalidClientError( + 'Invalid client: `redirect_uri` does not match client value', + ); + } + } + + return client; + } + + /** + * Validate requested scope. + */ + async validateScope(user, client, scope) { + if (this.model.validateScope) { + const validatedScope = await this.model.validateScope( + user, + client, + scope, + ); + + if (!validatedScope) { + throw new InvalidScopeError( + 'Invalid scope: Requested scope is invalid', + ); + } + + return validatedScope; + } + + return scope; + } + + /** + * Get scope from the request. + */ + + getScope(request) { + const scope = request.body.scope || request.query.scope; + + return parseScope(scope); + } + + /** + * Get state from the request. + */ + + getState(request) { + const state = request.body.state || request.query.state; + const stateExists = state && state.length > 0; + const stateIsValid = stateExists + ? isFormat.vschar(state) + : this.allowEmptyState; + + if (!stateIsValid) { + const message = !stateExists ? 'Missing' : 'Invalid'; + throw new InvalidRequestError(`${message} parameter: \`state\``); + } + + return state; + } + + /** + * Get user by calling the authenticate middleware. + */ + + async getUser(request, response) { + if (this.authenticateHandler instanceof AuthenticateHandler) { + const handled = await this.authenticateHandler.handle(request, response); + return handled ? handled.user : undefined; + } + + const user = await this.authenticateHandler.handle(request, response); + + if (!user) { + throw new ServerError( + 'Server error: `handle()` did not return a `user` object', + ); + } + + return user; + } + + /** + * Get redirect URI. + */ + + getRedirectUri(request, client) { + return ( + request.body.redirect_uri || + request.query.redirect_uri || + client.redirectUris[0] + ); + } + + /** + * Save authorization code. + */ + + async saveAuthorizationCode( + authorizationCode, + expiresAt, + scope, + client, + redirectUri, + user, + codeChallenge, + codeChallengeMethod, + ) { + let code = { + authorizationCode: authorizationCode, + expiresAt: expiresAt, + redirectUri: redirectUri, + scope: scope, + }; + + if (codeChallenge && codeChallengeMethod) { + code = Object.assign( + { + codeChallenge: codeChallenge, + codeChallengeMethod: codeChallengeMethod, + }, + code, + ); + } + + return this.model.saveAuthorizationCode(code, client, user); + } + + async validateRedirectUri(redirectUri, client) { + if (this.model.validateRedirectUri) { + return this.model.validateRedirectUri(redirectUri, client); + } + + return client.redirectUris.includes(redirectUri); + } + /** + * Get response type. + */ + + getResponseType(request) { + const responseType = + request.body.response_type || request.query.response_type; + + if (!responseType) { + throw new InvalidRequestError('Missing parameter: `response_type`'); + } + + if (!Object.prototype.hasOwnProperty.call(responseTypes, responseType)) { + throw new UnsupportedResponseTypeError( + 'Unsupported response type: `response_type` is not supported', + ); + } + + return responseTypes[responseType]; + } + + /** + * Build a successful response that redirects the user-agent to the client-provided url. + */ + + buildSuccessRedirectUri(redirectUri, responseType) { + return responseType.buildRedirectUri(redirectUri); + } + + /** + * Build an error response that redirects the user-agent to the client-provided url. + */ + + buildErrorRedirectUri(redirectUri, error) { + const uri = url.parse(redirectUri); + + uri.query = { + error: error.name, + }; + + if (error.message) { + uri.query.error_description = error.message; + } + + return uri; + } + + /** + * Update response with the redirect uri and the state parameter, if available. + */ + + updateResponse(response, redirectUri, state) { + redirectUri.query = redirectUri.query || {}; + + if (state) { + redirectUri.query.state = state; + } + + response.redirect(url.format(redirectUri)); + } + + getCodeChallenge(request) { + return request.body.code_challenge || request.query.code_challenge; + } + + /** + * Get code challenge method from request. + * + * When `enablePlainPKCE` is false (the default), the "plain" method is + * rejected and the default (when no method is provided) is "S256". + * When `enablePlainPKCE` is true, "plain" is accepted and used as the + * default per RFC 7636 §4.3. + * + * @see https://www.rfc-editor.org/rfc/rfc7636#section-4.3 + * @throws {InvalidRequestError} if request contains unsupported code_challenge_method + * (see https://www.rfc-editor.org/rfc/rfc7636#section-4.4) + */ + getCodeChallengeMethod(request) { + const algorithm = + request.body.code_challenge_method || request.query.code_challenge_method; + + if (algorithm && !pkce.isValidMethod(algorithm)) { + throw new InvalidRequestError( + `Invalid request: transform algorithm '${algorithm}' not supported`, + ); + } + + if (!this.enablePlainPKCE && algorithm === 'plain') { + throw new InvalidRequestError( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } + + // return the verified algorithm, if provided + if (algorithm) { + return algorithm; + } + + // otherwise, return the default algorithm based on the value of `enablePlainPKCE` + // which enables extended hardening by default, while + // optionally enable legacy support for the "plain" method per RFC 7636 §4.3 + return this.enablePlainPKCE ? 'plain' : 'S256'; + } } /** diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index d406d587..db56a6e6 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -1,33 +1,33 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const BearerTokenType = require("../token-types/bearer-token-type"); -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const InvalidClientError = require("../errors/invalid-client-error"); -const InvalidRequestError = require("../errors/invalid-request-error"); -const OAuthError = require("../errors/oauth-error"); -const Request = require("../request"); -const Response = require("../response"); -const ServerError = require("../errors/server-error"); -const TokenModel = require("../models/token-model"); -const UnauthorizedClientError = require("../errors/unauthorized-client-error"); -const UnsupportedGrantTypeError = require("../errors/unsupported-grant-type-error"); -const auth = require("basic-auth"); -const pkce = require("../pkce/pkce"); -const isFormat = require("@node-oauth/formats"); +const BearerTokenType = require('../token-types/bearer-token-type'); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const InvalidClientError = require('../errors/invalid-client-error'); +const InvalidRequestError = require('../errors/invalid-request-error'); +const OAuthError = require('../errors/oauth-error'); +const Request = require('../request'); +const Response = require('../response'); +const ServerError = require('../errors/server-error'); +const TokenModel = require('../models/token-model'); +const UnauthorizedClientError = require('../errors/unauthorized-client-error'); +const UnsupportedGrantTypeError = require('../errors/unsupported-grant-type-error'); +const auth = require('basic-auth'); +const pkce = require('../pkce/pkce'); +const isFormat = require('@node-oauth/formats'); /** * Grant types. */ const grantTypes = { - authorization_code: require("../grant-types/authorization-code-grant-type"), - client_credentials: require("../grant-types/client-credentials-grant-type"), - password: require("../grant-types/password-grant-type"), - refresh_token: require("../grant-types/refresh-token-grant-type"), + authorization_code: require('../grant-types/authorization-code-grant-type'), + client_credentials: require('../grant-types/client-credentials-grant-type'), + password: require('../grant-types/password-grant-type'), + refresh_token: require('../grant-types/refresh-token-grant-type'), }; /** @@ -35,315 +35,315 @@ const grantTypes = { */ class TokenHandler { - constructor(options) { - options = options || {}; - - if (!options.accessTokenLifetime) { - throw new InvalidArgumentError( - "Missing parameter: `accessTokenLifetime`", - ); - } - - if (!options.model) { - throw new InvalidArgumentError("Missing parameter: `model`"); - } - - if (!options.refreshTokenLifetime) { - throw new InvalidArgumentError( - "Missing parameter: `refreshTokenLifetime`", - ); - } - - if (!options.model.getClient) { - throw new InvalidArgumentError( - "Invalid argument: model does not implement `getClient()`", - ); - } - - this.accessTokenLifetime = options.accessTokenLifetime; - this.grantTypes = Object.assign({}, grantTypes, options.extendedGrantTypes); - this.model = options.model; - this.refreshTokenLifetime = options.refreshTokenLifetime; - this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; - this.requireClientAuthentication = - options.requireClientAuthentication || {}; - this.alwaysIssueNewRefreshToken = - options.alwaysIssueNewRefreshToken !== false; - this.enablePlainPKCE = options.enablePlainPKCE === true; - } - - /** - * Token Handler. - */ - - async handle(request, response) { - if (!(request instanceof Request)) { - throw new InvalidArgumentError( - "Invalid argument: `request` must be an instance of Request", - ); - } - - if (!(response instanceof Response)) { - throw new InvalidArgumentError( - "Invalid argument: `response` must be an instance of Response", - ); - } - - if (request.method !== "POST") { - throw new InvalidRequestError("Invalid request: method must be POST"); - } - - if (!request.is("application/x-www-form-urlencoded")) { - throw new InvalidRequestError( - "Invalid request: content must be application/x-www-form-urlencoded", - ); - } - - try { - const client = await this.getClient(request, response); - const data = await this.handleGrantType(request, client); - const model = new TokenModel(data, { - allowExtendedTokenAttributes: this.allowExtendedTokenAttributes, - }); - const tokenType = this.getTokenType(model); - - this.updateSuccessResponse(response, tokenType); - - return data; - } catch (err) { - let e = err; - - if (!(e instanceof OAuthError)) { - e = new ServerError(e); - } - - this.updateErrorResponse(response, e); - throw e; - } - } - - /** - * Get the client from the model. - */ - - async getClient(request, response) { - const credentials = await this.getClientCredentials(request); - const grantType = request.body.grant_type; - const codeVerifier = request.body.code_verifier; - const isPkce = pkce.isPKCERequest({ grantType, codeVerifier }); - - if (!credentials.clientId) { - throw new InvalidRequestError("Missing parameter: `client_id`"); - } - - if ( - this.isClientAuthenticationRequired(grantType) && - !credentials.clientSecret && - !isPkce - ) { - throw new InvalidRequestError("Missing parameter: `client_secret`"); - } - - if (!isFormat.vschar(credentials.clientId)) { - throw new InvalidRequestError("Invalid parameter: `client_id`"); - } - - if ( - credentials.clientSecret && - !isFormat.vschar(credentials.clientSecret) - ) { - throw new InvalidRequestError("Invalid parameter: `client_secret`"); - } - - try { - const client = await this.model.getClient( - credentials.clientId, - credentials.clientSecret, - ); - - if (!client) { - throw new InvalidClientError("Invalid client: client is invalid"); - } - - if (!client.grants) { - throw new ServerError("Server error: missing client `grants`"); - } - - if (!(client.grants instanceof Array)) { - throw new ServerError("Server error: `grants` must be an array"); - } - - return client; - } catch (e) { - // Include the "WWW-Authenticate" response header field if the client - // attempted to authenticate via the "Authorization" request header. - // - // @see https://tools.ietf.org/html/rfc6749#section-5.2. - if (e instanceof InvalidClientError && request.get("authorization")) { - response.set("WWW-Authenticate", 'Basic realm="Service"'); - throw new InvalidClientError(e, { code: 401 }); - } - - throw e; - } - } - - /** - * Get client credentials. - * - * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, - * the `client_id` and `client_secret` can be embedded in the body. - * - * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 - */ - - getClientCredentials(request) { - const credentials = auth(request); - const grantType = request.body.grant_type; - const codeVerifier = request.body.code_verifier; - - if (credentials) { - return { clientId: credentials.name, clientSecret: credentials.pass }; - } - - if (request.body.client_id && request.body.client_secret) { - return { - clientId: request.body.client_id, - clientSecret: request.body.client_secret, - }; - } - - if (pkce.isPKCERequest({ grantType, codeVerifier })) { - if (request.body.client_id) { - return { clientId: request.body.client_id }; - } - } - - if (!this.isClientAuthenticationRequired(grantType)) { - if (request.body.client_id) { - return { clientId: request.body.client_id }; - } - } - - throw new InvalidClientError( - "Invalid client: cannot retrieve client credentials", - ); - } - - /** - * Handle grant type. - */ - - async handleGrantType(request, client) { - const grantType = request.body.grant_type; - - if (!grantType) { - throw new InvalidRequestError("Missing parameter: `grant_type`"); - } - - if (!isFormat.nchar(grantType) && !isFormat.uri(grantType)) { - throw new InvalidRequestError("Invalid parameter: `grant_type`"); - } - - if (!Object.prototype.hasOwnProperty.call(this.grantTypes, grantType)) { - throw new UnsupportedGrantTypeError( - "Unsupported grant type: `grant_type` is invalid", - ); - } - - if (!Array.isArray(client.grants) || !client.grants.includes(grantType)) { - throw new UnauthorizedClientError( - "Unauthorized client: `grant_type` is invalid", - ); - } - - const accessTokenLifetime = this.getAccessTokenLifetime(client); - const refreshTokenLifetime = this.getRefreshTokenLifetime(client); - const Type = this.grantTypes[grantType]; - - const options = { - accessTokenLifetime: accessTokenLifetime, - model: this.model, - refreshTokenLifetime: refreshTokenLifetime, - alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken, - enablePlainPKCE: this.enablePlainPKCE === true, - }; - - return new Type(options).handle(request, client); - } - - /** - * Get access token lifetime. - */ - - getAccessTokenLifetime(client) { - return client.accessTokenLifetime || this.accessTokenLifetime; - } - - /** - * Get refresh token lifetime. - */ - - getRefreshTokenLifetime(client) { - return client.refreshTokenLifetime || this.refreshTokenLifetime; - } - - /** - * Get token type. - */ - - getTokenType(model) { - return new BearerTokenType( - model.accessToken, - model.accessTokenLifetime, - model.refreshToken, - model.scope, - model.customAttributes, - ); - } - - /** - * Update response when a token is generated. - */ - - updateSuccessResponse(response, tokenType) { - response.body = tokenType.valueOf(); - - // for compliance reasons we rebuild the internal scope to be a string - // https://datatracker.ietf.org/doc/html/rfc6749.html#section-5.1 - if (response.body.scope) { - response.body.scope = response.body.scope.join(" "); - } - - response.set("Cache-Control", "no-store"); - response.set("Pragma", "no-cache"); - } - - /** - * Update response when an error is thrown. - */ - - updateErrorResponse(response, error) { - response.body = { - error: error.name, - error_description: error.message, - }; - - response.status = error.code; - } - - /** - * Given a grant type, check if client authentication is required - */ - isClientAuthenticationRequired(grantType) { - if (Object.keys(this.requireClientAuthentication).length > 0) { - return typeof this.requireClientAuthentication[grantType] !== "undefined" - ? this.requireClientAuthentication[grantType] - : true; - } else { - return true; - } - } + constructor(options) { + options = options || {}; + + if (!options.accessTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `accessTokenLifetime`', + ); + } + + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } + + if (!options.refreshTokenLifetime) { + throw new InvalidArgumentError( + 'Missing parameter: `refreshTokenLifetime`', + ); + } + + if (!options.model.getClient) { + throw new InvalidArgumentError( + 'Invalid argument: model does not implement `getClient()`', + ); + } + + this.accessTokenLifetime = options.accessTokenLifetime; + this.grantTypes = Object.assign({}, grantTypes, options.extendedGrantTypes); + this.model = options.model; + this.refreshTokenLifetime = options.refreshTokenLifetime; + this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; + this.requireClientAuthentication = + options.requireClientAuthentication || {}; + this.alwaysIssueNewRefreshToken = + options.alwaysIssueNewRefreshToken !== false; + this.enablePlainPKCE = options.enablePlainPKCE === true; + } + + /** + * Token Handler. + */ + + async handle(request, response) { + if (!(request instanceof Request)) { + throw new InvalidArgumentError( + 'Invalid argument: `request` must be an instance of Request', + ); + } + + if (!(response instanceof Response)) { + throw new InvalidArgumentError( + 'Invalid argument: `response` must be an instance of Response', + ); + } + + if (request.method !== 'POST') { + throw new InvalidRequestError('Invalid request: method must be POST'); + } + + if (!request.is('application/x-www-form-urlencoded')) { + throw new InvalidRequestError( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + + try { + const client = await this.getClient(request, response); + const data = await this.handleGrantType(request, client); + const model = new TokenModel(data, { + allowExtendedTokenAttributes: this.allowExtendedTokenAttributes, + }); + const tokenType = this.getTokenType(model); + + this.updateSuccessResponse(response, tokenType); + + return data; + } catch (err) { + let e = err; + + if (!(e instanceof OAuthError)) { + e = new ServerError(e); + } + + this.updateErrorResponse(response, e); + throw e; + } + } + + /** + * Get the client from the model. + */ + + async getClient(request, response) { + const credentials = await this.getClientCredentials(request); + const grantType = request.body.grant_type; + const codeVerifier = request.body.code_verifier; + const isPkce = pkce.isPKCERequest({ grantType, codeVerifier }); + + if (!credentials.clientId) { + throw new InvalidRequestError('Missing parameter: `client_id`'); + } + + if ( + this.isClientAuthenticationRequired(grantType) && + !credentials.clientSecret && + !isPkce + ) { + throw new InvalidRequestError('Missing parameter: `client_secret`'); + } + + if (!isFormat.vschar(credentials.clientId)) { + throw new InvalidRequestError('Invalid parameter: `client_id`'); + } + + if ( + credentials.clientSecret && + !isFormat.vschar(credentials.clientSecret) + ) { + throw new InvalidRequestError('Invalid parameter: `client_secret`'); + } + + try { + const client = await this.model.getClient( + credentials.clientId, + credentials.clientSecret, + ); + + if (!client) { + throw new InvalidClientError('Invalid client: client is invalid'); + } + + if (!client.grants) { + throw new ServerError('Server error: missing client `grants`'); + } + + if (!(client.grants instanceof Array)) { + throw new ServerError('Server error: `grants` must be an array'); + } + + return client; + } catch (e) { + // Include the "WWW-Authenticate" response header field if the client + // attempted to authenticate via the "Authorization" request header. + // + // @see https://tools.ietf.org/html/rfc6749#section-5.2. + if (e instanceof InvalidClientError && request.get('authorization')) { + response.set('WWW-Authenticate', 'Basic realm="Service"'); + throw new InvalidClientError(e, { code: 401 }); + } + + throw e; + } + } + + /** + * Get client credentials. + * + * The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively, + * the `client_id` and `client_secret` can be embedded in the body. + * + * @see https://tools.ietf.org/html/rfc6749#section-2.3.1 + */ + + getClientCredentials(request) { + const credentials = auth(request); + const grantType = request.body.grant_type; + const codeVerifier = request.body.code_verifier; + + if (credentials) { + return { clientId: credentials.name, clientSecret: credentials.pass }; + } + + if (request.body.client_id && request.body.client_secret) { + return { + clientId: request.body.client_id, + clientSecret: request.body.client_secret, + }; + } + + if (pkce.isPKCERequest({ grantType, codeVerifier })) { + if (request.body.client_id) { + return { clientId: request.body.client_id }; + } + } + + if (!this.isClientAuthenticationRequired(grantType)) { + if (request.body.client_id) { + return { clientId: request.body.client_id }; + } + } + + throw new InvalidClientError( + 'Invalid client: cannot retrieve client credentials', + ); + } + + /** + * Handle grant type. + */ + + async handleGrantType(request, client) { + const grantType = request.body.grant_type; + + if (!grantType) { + throw new InvalidRequestError('Missing parameter: `grant_type`'); + } + + if (!isFormat.nchar(grantType) && !isFormat.uri(grantType)) { + throw new InvalidRequestError('Invalid parameter: `grant_type`'); + } + + if (!Object.prototype.hasOwnProperty.call(this.grantTypes, grantType)) { + throw new UnsupportedGrantTypeError( + 'Unsupported grant type: `grant_type` is invalid', + ); + } + + if (!Array.isArray(client.grants) || !client.grants.includes(grantType)) { + throw new UnauthorizedClientError( + 'Unauthorized client: `grant_type` is invalid', + ); + } + + const accessTokenLifetime = this.getAccessTokenLifetime(client); + const refreshTokenLifetime = this.getRefreshTokenLifetime(client); + const Type = this.grantTypes[grantType]; + + const options = { + accessTokenLifetime: accessTokenLifetime, + model: this.model, + refreshTokenLifetime: refreshTokenLifetime, + alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken, + enablePlainPKCE: this.enablePlainPKCE === true, + }; + + return new Type(options).handle(request, client); + } + + /** + * Get access token lifetime. + */ + + getAccessTokenLifetime(client) { + return client.accessTokenLifetime || this.accessTokenLifetime; + } + + /** + * Get refresh token lifetime. + */ + + getRefreshTokenLifetime(client) { + return client.refreshTokenLifetime || this.refreshTokenLifetime; + } + + /** + * Get token type. + */ + + getTokenType(model) { + return new BearerTokenType( + model.accessToken, + model.accessTokenLifetime, + model.refreshToken, + model.scope, + model.customAttributes, + ); + } + + /** + * Update response when a token is generated. + */ + + updateSuccessResponse(response, tokenType) { + response.body = tokenType.valueOf(); + + // for compliance reasons we rebuild the internal scope to be a string + // https://datatracker.ietf.org/doc/html/rfc6749.html#section-5.1 + if (response.body.scope) { + response.body.scope = response.body.scope.join(' '); + } + + response.set('Cache-Control', 'no-store'); + response.set('Pragma', 'no-cache'); + } + + /** + * Update response when an error is thrown. + */ + + updateErrorResponse(response, error) { + response.body = { + error: error.name, + error_description: error.message, + }; + + response.status = error.code; + } + + /** + * Given a grant type, check if client authentication is required + */ + isClientAuthenticationRequired(grantType) { + if (Object.keys(this.requireClientAuthentication).length > 0) { + return typeof this.requireClientAuthentication[grantType] !== 'undefined' + ? this.requireClientAuthentication[grantType] + : true; + } else { + return true; + } + } } /** diff --git a/lib/model.js b/lib/model.js index eba1f522..c634309b 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1,9 +1,9 @@ -"use strict"; +'use strict'; /* * Module dependencies */ -const ServerError = require("./errors/server-error"); +const ServerError = require('./errors/server-error'); /** * @typedef AccessTokenData @@ -64,170 +64,170 @@ const ServerError = require("./errors/server-error"); * }) */ class Model { - // eslint-disable-line no-unused-vars - /** - * Factory function to create a model form your implementation. - * @static - * @param impl {object} an object containing your model function implementations - * @return {Model} the model instance. - */ - static from(impl) { - const m = new Model(); - const nullFns = {}; - Object.getOwnPropertyNames(Model.prototype).forEach((key) => { - nullFns[key] = null; - }); - Object.assign(m, nullFns, impl); - return m; - } + // eslint-disable-line no-unused-vars + /** + * Factory function to create a model form your implementation. + * @static + * @param impl {object} an object containing your model function implementations + * @return {Model} the model instance. + */ + static from(impl) { + const m = new Model(); + const nullFns = {}; + Object.getOwnPropertyNames(Model.prototype).forEach((key) => { + nullFns[key] = null; + }); + Object.assign(m, nullFns, impl); + return m; + } - /*------------------------------------------------------------------------- + /*------------------------------------------------------------------------- | ALWAYS REQUIRED *------------------------------------------------------------------------- | The following functions are required by every workflow / grant type. */ - /** - * Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type. - * This model function is **required** for all grant types. - * **Invoked during:** - * - * - `authorization_code` grant - * - `client_credentials` grant - * - `refresh_token` grant - * - `password` grant - * - * - * @async - * @param clientId {string} The client id of the client to retrieve. - * @param clientSecret {string?} The client secret of the client to retrieve. Can be `null`. - * @returns {Promise.} - * @fulfil {ClientData} - An `Object` representing the client and associated data, or a falsy value if no such client could be found. - * @reject {Error} - An Error type - */ - async getClient(clientId, clientSecret) { - throw new ServerError("getClient not implemented"); - } + /** + * Invoked to retrieve a client using a client id or a client id/client secret combination, depending on the grant type. + * This model function is **required** for all grant types. + * **Invoked during:** + * + * - `authorization_code` grant + * - `client_credentials` grant + * - `refresh_token` grant + * - `password` grant + * + * + * @async + * @param clientId {string} The client id of the client to retrieve. + * @param clientSecret {string?} The client secret of the client to retrieve. Can be `null`. + * @returns {Promise.} + * @fulfil {ClientData} - An `Object` representing the client and associated data, or a falsy value if no such client could be found. + * @reject {Error} - An Error type + */ + async getClient(clientId, clientSecret) { + throw new ServerError('getClient not implemented'); + } - /** - * Invoked to save an access token and optionally a refresh token, depending on the grant type. - * This model function is **required** for all grant types. - * - * **Invoked during:** - * - `authorization_code` grant - * - `client_credentials` grant - * - `refresh_token` grant - * - `password` grant - * - * If the `allowExtendedTokenAttributes` server option is enabled (see `OAuth2Server#token() `) any additional attributes set on the result are copied to the token response sent to the client. - * - * @async - * @instance - * @param token {object} The token(s) to be saved. - * @param token.accessToken {string} The access token to be saved. - * @param token.accessTokenExpiresAt {Date} The expiry time of the access token. - * @param token.refreshToken {string} The refresh token to be saved. - * @param token.refreshTokenExpiresAt {Date} The expiry time of the refresh token. - * @param token.scope {string[]} The authorized scope of the token(s) - * @param client {ClientData} The client associated with the token(s). - * @param user {object} The user associated with the token(s). - * @return {Promise} - * @fulfil {{accessToken:string,accessTokenExpiresAt:Date,refreshToken: string,refreshTokenExpiresAt: Date,scope: string[],client: ClientData,user: object}} An `Object` representing the token(s) and associated data. - * @example - * function saveToken(token, client, user) { - * // imaginary DB queries - * let fns = [ - * db.saveAccessToken({ - * access_token: token.accessToken, - * expires_at: token.accessTokenExpiresAt, - * scope: token.scope, - * client_id: client.id, - * user_id: user.id - * }), - * db.saveRefreshToken({ - * refresh_token: token.refreshToken, - * expires_at: token.refreshTokenExpiresAt, - * scope: token.scope, - * client_id: client.id, - * user_id: user.id - * }) - * ]; - * return Promise.all(fns); - * .spread(function(accessToken, refreshToken) { - * return { - * accessToken: accessToken.access_token, - * accessTokenExpiresAt: accessToken.expires_at, - * refreshToken: refreshToken.refresh_token, - * refreshTokenExpiresAt: refreshToken.expires_at, - * scope: accessToken.scope, - * client: {id: accessToken.client_id}, - * user: {id: accessToken.user_id} - * }; - * }); - * } - */ - async saveToken(token, client, user) { - throw new ServerError("saveToken not implemented"); - } + /** + * Invoked to save an access token and optionally a refresh token, depending on the grant type. + * This model function is **required** for all grant types. + * + * **Invoked during:** + * - `authorization_code` grant + * - `client_credentials` grant + * - `refresh_token` grant + * - `password` grant + * + * If the `allowExtendedTokenAttributes` server option is enabled (see `OAuth2Server#token() `) any additional attributes set on the result are copied to the token response sent to the client. + * + * @async + * @instance + * @param token {object} The token(s) to be saved. + * @param token.accessToken {string} The access token to be saved. + * @param token.accessTokenExpiresAt {Date} The expiry time of the access token. + * @param token.refreshToken {string} The refresh token to be saved. + * @param token.refreshTokenExpiresAt {Date} The expiry time of the refresh token. + * @param token.scope {string[]} The authorized scope of the token(s) + * @param client {ClientData} The client associated with the token(s). + * @param user {object} The user associated with the token(s). + * @return {Promise} + * @fulfil {{accessToken:string,accessTokenExpiresAt:Date,refreshToken: string,refreshTokenExpiresAt: Date,scope: string[],client: ClientData,user: object}} An `Object` representing the token(s) and associated data. + * @example + * function saveToken(token, client, user) { + * // imaginary DB queries + * let fns = [ + * db.saveAccessToken({ + * access_token: token.accessToken, + * expires_at: token.accessTokenExpiresAt, + * scope: token.scope, + * client_id: client.id, + * user_id: user.id + * }), + * db.saveRefreshToken({ + * refresh_token: token.refreshToken, + * expires_at: token.refreshTokenExpiresAt, + * scope: token.scope, + * client_id: client.id, + * user_id: user.id + * }) + * ]; + * return Promise.all(fns); + * .spread(function(accessToken, refreshToken) { + * return { + * accessToken: accessToken.access_token, + * accessTokenExpiresAt: accessToken.expires_at, + * refreshToken: refreshToken.refresh_token, + * refreshTokenExpiresAt: refreshToken.expires_at, + * scope: accessToken.scope, + * client: {id: accessToken.client_id}, + * user: {id: accessToken.user_id} + * }; + * }); + * } + */ + async saveToken(token, client, user) { + throw new ServerError('saveToken not implemented'); + } - /*------------------------------------------------------------------------- + /*------------------------------------------------------------------------- | PARTIALLY REQUIRED *------------------------------------------------------------------------- | The following functions are required by specific grant types or under | specific conditions. */ - /** - * Invoked to retrieve a user using a username/password combination. - * This model function is **required** if the `password` grant is used. - * Please note, that password grant is considered unsafe. - * It is still supported but marked deprecated. - * - * **Invoked during:** - * - `password` grant - * - * @deprecated - * @async - * @param username {string} The username of the user to retrieve. - * @param password {string} The user's password. - * @param client {ClientData=} The client. - * @return {Promise} An `Object` representing the user, or a falsy value if no such user could be found. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. - * @example - * function getUser(username, password) { - * // imaginary DB query - * return db.queryUser({username: username, password: password}); - * } - */ - async getUser(username, password, client) { - throw new ServerError("getUser not implemented"); - } + /** + * Invoked to retrieve a user using a username/password combination. + * This model function is **required** if the `password` grant is used. + * Please note, that password grant is considered unsafe. + * It is still supported but marked deprecated. + * + * **Invoked during:** + * - `password` grant + * + * @deprecated + * @async + * @param username {string} The username of the user to retrieve. + * @param password {string} The user's password. + * @param client {ClientData=} The client. + * @return {Promise} An `Object` representing the user, or a falsy value if no such user could be found. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. + * @example + * function getUser(username, password) { + * // imaginary DB query + * return db.queryUser({username: username, password: password}); + * } + */ + async getUser(username, password, client) { + throw new ServerError('getUser not implemented'); + } - /** - * Invoked to retrieve the user associated with the specified client. - * This model function is **required** if the `client_credentials` grant is used. - * - * **Invoked during:** - * - `client_credentials` grant - * - * **Remarks:** - * - * `client` is the object previously obtained through `Model#getClient() `. - * - * @async - * @instance - * @param client {ClientData} The client to retrieve the associated user for. - * @return {Promise} An `Object` representing the user, or a falsy value if the client does not have an associated user. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. - * @example - * function getUserFromClient(client) { - * // imaginary DB query - * return db.queryUser({id: client.user_id}); - * } - */ - async getUserFromClient(client) { - throw new ServerError("getUserFromClient not implemented"); - } + /** + * Invoked to retrieve the user associated with the specified client. + * This model function is **required** if the `client_credentials` grant is used. + * + * **Invoked during:** + * - `client_credentials` grant + * + * **Remarks:** + * + * `client` is the object previously obtained through `Model#getClient() `. + * + * @async + * @instance + * @param client {ClientData} The client to retrieve the associated user for. + * @return {Promise} An `Object` representing the user, or a falsy value if the client does not have an associated user. The user object is completely transparent to *oauth2-server* and is simply used as input to other model functions. + * @example + * function getUserFromClient(client) { + * // imaginary DB query + * return db.queryUser({id: client.user_id}); + * } + */ + async getUserFromClient(client) { + throw new ServerError('getUserFromClient not implemented'); + } - /** + /** * Invoked to retrieve an existing access token, including associated data, that has previously been saved through `Model#saveToken() `. * This model function is **required** if `OAuth2Server#authenticate() ` is used. * @@ -261,11 +261,11 @@ class Model { * }); * } */ - async getAccessToken(accessToken) { - throw new ServerError("getAccessToken not implemented"); - } + async getAccessToken(accessToken) { + throw new ServerError('getAccessToken not implemented'); + } - /** + /** * Invoked to retrieve an existing refresh token previously saved through `Model#saveToken() `. * This model function is **required** if the `refresh_token` grant is used. * **Invoked during:** @@ -298,11 +298,11 @@ class Model { * }); * } */ - async getRefreshToken(refreshToken) { - throw new ServerError("getRefreshToken not implemented"); - } + async getRefreshToken(refreshToken) { + throw new ServerError('getRefreshToken not implemented'); + } - /** + /** * Invoked to retrieve an existing authorization code previously saved through `Model#saveAuthorizationCode() `. * This model function is **required** if the `authorization_code` grant is used. * **Invoked during:** @@ -337,11 +337,11 @@ class Model { * }); * } */ - async getAuthorizationCode(authorizationCode) { - throw new ServerError("getAuthorizationCode not implemented"); - } + async getAuthorizationCode(authorizationCode) { + throw new ServerError('getAuthorizationCode not implemented'); + } - /** + /** * Invoked to save an authorization code. * This model function is **required** if the `authorization_code` grant is used. * @@ -384,11 +384,11 @@ class Model { * }); * } */ - async saveAuthorizationCode(code, client, user) { - throw new ServerError("saveAuthorizationCode not implemented"); - } + async saveAuthorizationCode(code, client, user) { + throw new ServerError('saveAuthorizationCode not implemented'); + } - /** + /** * Invoked to revoke a refresh token. * This model function is **required** if the `refresh_token` grant is used. * **Invoked during:** @@ -410,30 +410,30 @@ class Model { * }); * } */ - async revokeToken(token) { - throw new ServerError("revokeToken not implemented"); - } + async revokeToken(token) { + throw new ServerError('revokeToken not implemented'); + } - /** - * Invoked to revoke an authorization code. - * This model function is **required** if the `authorization_code` grant is used. - * - * **Invoked during:** - * - `authorization_code` grant - * - * **Remarks:** - * `code` is the authorization code object previously obtained through {@link Model#getAuthorizationCode}. - * - * @async - * @method - * @param code {AuthorizationCodeData} - * @return {Promise} Return `true` if the revocation was successful or `false` if the authorization code could not be found. - */ - async revokeAuthorizationCode(code) { - throw new ServerError("revokeAuthorizationCode not implemented"); - } + /** + * Invoked to revoke an authorization code. + * This model function is **required** if the `authorization_code` grant is used. + * + * **Invoked during:** + * - `authorization_code` grant + * + * **Remarks:** + * `code` is the authorization code object previously obtained through {@link Model#getAuthorizationCode}. + * + * @async + * @method + * @param code {AuthorizationCodeData} + * @return {Promise} Return `true` if the revocation was successful or `false` if the authorization code could not be found. + */ + async revokeAuthorizationCode(code) { + throw new ServerError('revokeAuthorizationCode not implemented'); + } - /** + /** * Invoked during request authentication to check if the provided access token was authorized the requested scopes. * * This model function is **required** if scopes are used with `OAuth2Server#authenticate() ` @@ -460,17 +460,17 @@ class Model { * return requestedScopes.every(s => authorizedScopes.includes(s)); * } */ - async verifyScope(accessToken, scope) { - throw new ServerError("verifyScope not implemented"); - } + async verifyScope(accessToken, scope) { + throw new ServerError('verifyScope not implemented'); + } - /*------------------------------------------------------------------------- + /*------------------------------------------------------------------------- | OPTIONAL *------------------------------------------------------------------------- | The following functions are entirely optional */ - /** + /** * Invoked to generate a new access token. * This model function is **optional**. * @@ -494,11 +494,11 @@ class Model { * @param scope {string[]?} The scopes associated with the token. Can be `null` * @return {Promise.} A `String` to be used as access token. */ - async generateAccessToken(client, user, scope) { - throw new ServerError("generateAccessToken not implemented"); - } + async generateAccessToken(client, user, scope) { + throw new ServerError('generateAccessToken not implemented'); + } - /** + /** * Invoked to generate a new refresh token. * * This model function is **optional**. If not implemented, a default handler is used that generates refresh tokens consisting of 40 characters in the range of `a..z0..9`. @@ -523,11 +523,11 @@ class Model { * @param scope {string[]?} The scopes associated with the refresh token. Can be `null` * @return {Promise} A `String` to be used as refresh token. */ - async generateRefreshToken(client, user, scope) { - throw new ServerError("generateRefreshToken not implemented"); - } + async generateRefreshToken(client, user, scope) { + throw new ServerError('generateRefreshToken not implemented'); + } - /** + /** * Invoked to generate a new authorization code. * This model function is **optional**. If not implemented, a default handler is used that generates authorization codes consisting of 40 characters in the range of `a..z0..9`. * [RFC 6749 Appendix A.11](https://www.rfc-editor.org/6749#appendix-A.11) specifies that authorization codes must consist of characters inside the range `0x20..0x7E` (i.e. only printable US-ASCII characters). @@ -544,11 +544,11 @@ class Model { * @return {Promise} A `String` to be used as authorization code. * */ - async generateAuthorizationCode(client, user, scope) { - throw new ServerError("generateAuthorizationCode not implemented"); - } + async generateAuthorizationCode(client, user, scope) { + throw new ServerError('generateAuthorizationCode not implemented'); + } - /** + /** * Invoked to check if the requested `scope` is valid for a particular `client`/`user` combination. * * This model function is **optional**. If not implemented, any scope is accepted. @@ -589,11 +589,11 @@ class Model { * return scope.filter(s => VALID_SCOPES.indexOf(s) >= 0); * } */ - async validateScope(user, client, scope) { - throw new ServerError("validateScope not implemented"); - } + async validateScope(user, client, scope) { + throw new ServerError('validateScope not implemented'); + } - /** + /** * Invoked to check if the provided `redirectUri` is valid for a particular `client`. * This model function is **optional**. If not implemented, the `redirectUri` should be included in the provided `redirectUris` of the client. * @@ -611,9 +611,9 @@ class Model { * @param client {object} The associated client. * @return {Promise} Returns `true` if the `redirectUri` is valid, `false` otherwise. */ - async validateRedirectUri(redirectUri, client) { - throw new ServerError("validateRedirectUri not implemented"); - } + async validateRedirectUri(redirectUri, client) { + throw new ServerError('validateRedirectUri not implemented'); + } } module.exports = Model; diff --git a/lib/models/token-model.js b/lib/models/token-model.js index e235f4de..d7ac117d 100644 --- a/lib/models/token-model.js +++ b/lib/models/token-model.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const { getLifetimeFromExpiresAt } = require("../utils/date-util"); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const { getLifetimeFromExpiresAt } = require('../utils/date-util'); /** * @private @@ -13,13 +13,13 @@ const { getLifetimeFromExpiresAt } = require("../utils/date-util"); * @description The core model attributes allowed when `allowExtendedTokenAttributes` is `false`. */ const modelAttributes = new Set([ - "accessToken", - "accessTokenExpiresAt", - "refreshToken", - "refreshTokenExpiresAt", - "scope", - "client", - "user", + 'accessToken', + 'accessTokenExpiresAt', + 'refreshToken', + 'refreshTokenExpiresAt', + 'scope', + 'client', + 'user', ]); /** @@ -27,70 +27,70 @@ const modelAttributes = new Set([ * @classDesc */ class TokenModel { - /** - * @constructor - * @param data - * @param options - */ - constructor(data = {}, options = {}) { - const { - accessToken, - accessTokenExpiresAt, - refreshToken, - refreshTokenExpiresAt, - scope, - client, - user, - } = data; + /** + * @constructor + * @param data + * @param options + */ + constructor(data = {}, options = {}) { + const { + accessToken, + accessTokenExpiresAt, + refreshToken, + refreshTokenExpiresAt, + scope, + client, + user, + } = data; - if (!accessToken) { - throw new InvalidArgumentError("Missing parameter: `accessToken`"); - } + if (!accessToken) { + throw new InvalidArgumentError('Missing parameter: `accessToken`'); + } - if (!client) { - throw new InvalidArgumentError("Missing parameter: `client`"); - } + if (!client) { + throw new InvalidArgumentError('Missing parameter: `client`'); + } - if (!user) { - throw new InvalidArgumentError("Missing parameter: `user`"); - } + if (!user) { + throw new InvalidArgumentError('Missing parameter: `user`'); + } - if (accessTokenExpiresAt && !(accessTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError( - "Invalid parameter: `accessTokenExpiresAt`", - ); - } + if (accessTokenExpiresAt && !(accessTokenExpiresAt instanceof Date)) { + throw new InvalidArgumentError( + 'Invalid parameter: `accessTokenExpiresAt`', + ); + } - if (refreshTokenExpiresAt && !(refreshTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError( - "Invalid parameter: `refreshTokenExpiresAt`", - ); - } + if (refreshTokenExpiresAt && !(refreshTokenExpiresAt instanceof Date)) { + throw new InvalidArgumentError( + 'Invalid parameter: `refreshTokenExpiresAt`', + ); + } - this.accessToken = accessToken; - this.accessTokenExpiresAt = accessTokenExpiresAt; - this.client = client; - this.refreshToken = refreshToken; - this.refreshTokenExpiresAt = refreshTokenExpiresAt; - this.scope = scope; - this.user = user; + this.accessToken = accessToken; + this.accessTokenExpiresAt = accessTokenExpiresAt; + this.client = client; + this.refreshToken = refreshToken; + this.refreshTokenExpiresAt = refreshTokenExpiresAt; + this.scope = scope; + this.user = user; - if (accessTokenExpiresAt) { - this.accessTokenLifetime = getLifetimeFromExpiresAt(accessTokenExpiresAt); - } + if (accessTokenExpiresAt) { + this.accessTokenLifetime = getLifetimeFromExpiresAt(accessTokenExpiresAt); + } - const { allowExtendedTokenAttributes } = options; + const { allowExtendedTokenAttributes } = options; - if (allowExtendedTokenAttributes) { - this.customAttributes = {}; + if (allowExtendedTokenAttributes) { + this.customAttributes = {}; - Object.keys(data).forEach((key) => { - if (!modelAttributes.has(key)) { - this.customAttributes[key] = data[key]; - } - }); - } - } + Object.keys(data).forEach((key) => { + if (!modelAttributes.has(key)) { + this.customAttributes[key] = data[key]; + } + }); + } + } } module.exports = TokenModel; diff --git a/lib/pkce/pkce.js b/lib/pkce/pkce.js index c0b7cdaa..a822a448 100644 --- a/lib/pkce/pkce.js +++ b/lib/pkce/pkce.js @@ -1,10 +1,10 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const { base64URLEncode } = require("../utils/string-util"); -const { createHash } = require("../utils/crypto-util"); +const { base64URLEncode } = require('../utils/string-util'); +const { createHash } = require('../utils/crypto-util'); /** * ABNF for "code_verifier" and "code_challenge" is as follows. @@ -16,7 +16,7 @@ const { createHash } = require("../utils/crypto-util"); * @type {RegExp} */ const codeChallengeAndVerifierRegexp = - /^([\u0041-\u005a\u0061-\u007A0-9.\-_~]){43,128}$/; + /^([\u0041-\u005a\u0061-\u007A0-9.\-_~]){43,128}$/; /** * @module pkce @@ -31,22 +31,22 @@ const codeChallengeAndVerifierRegexp = * @return {String|undefined} */ function getHashForCodeChallenge({ method, verifier }) { - // to prevent undesired side-effects when passing some weird values - // to createHash or base64URLEncode we first check if the values are right - if ( - isValidMethod(method) && - typeof verifier === "string" && - verifier.length > 0 - ) { - if (method === "plain") { - return verifier; - } + // to prevent undesired side-effects when passing some weird values + // to createHash or base64URLEncode we first check if the values are right + if ( + isValidMethod(method) && + typeof verifier === 'string' && + verifier.length > 0 + ) { + if (method === 'plain') { + return verifier; + } - if (method === "S256") { - const hash = createHash({ data: verifier }); - return base64URLEncode(hash); - } - } + if (method === 'S256') { + const hash = createHash({ data: verifier }); + return base64URLEncode(hash); + } + } } /** @@ -62,10 +62,10 @@ function getHashForCodeChallenge({ method, verifier }) { * @return {Boolean} */ function codeChallengeMatchesABNF(codeChallenge) { - return ( - typeof codeChallenge === "string" && - codeChallengeAndVerifierRegexp.test(codeChallenge) - ); + return ( + typeof codeChallenge === 'string' && + codeChallengeAndVerifierRegexp.test(codeChallenge) + ); } /** @@ -77,7 +77,7 @@ function codeChallengeMatchesABNF(codeChallenge) { * @return {boolean} */ function isPKCERequest({ grantType, codeVerifier }) { - return grantType === "authorization_code" && !!codeVerifier; + return grantType === 'authorization_code' && !!codeVerifier; } /** @@ -88,12 +88,12 @@ function isPKCERequest({ grantType, codeVerifier }) { * @return {boolean} */ function isValidMethod(method) { - return method === "S256" || method === "plain"; + return method === 'S256' || method === 'plain'; } module.exports = { - getHashForCodeChallenge, - codeChallengeMatchesABNF, - isPKCERequest, - isValidMethod, + getHashForCodeChallenge, + codeChallengeMatchesABNF, + isPKCERequest, + isValidMethod, }; diff --git a/lib/request.js b/lib/request.js index 7f4e6c9b..c1cd37e8 100644 --- a/lib/request.js +++ b/lib/request.js @@ -1,11 +1,11 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const InvalidArgumentError = require("./errors/invalid-argument-error"); +const InvalidArgumentError = require('./errors/invalid-argument-error'); /* type-is: https://github.com/jshttp/type-is */ -const typeis = require("type-is"); +const typeis = require('type-is'); /** * Wrapper for webserver's request. @@ -19,64 +19,64 @@ const typeis = require("type-is"); * } */ class Request { - /** - * Creates a new request instance - * @constructor - * @param headers {object} key-value object of headers - * @param method {string} the HTTP method - * @param query {object} key-value object of query parameters - * @param body {object=} optional key-value object of body parameters - * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver - * @throws {InvalidArgumentError} if one of headers, method or query are missing. - */ - constructor({ headers, method, query, body, ...otherOptions } = {}) { - if (!headers) { - throw new InvalidArgumentError("Missing parameter: `headers`"); - } + /** + * Creates a new request instance + * @constructor + * @param headers {object} key-value object of headers + * @param method {string} the HTTP method + * @param query {object} key-value object of query parameters + * @param body {object=} optional key-value object of body parameters + * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver + * @throws {InvalidArgumentError} if one of headers, method or query are missing. + */ + constructor({ headers, method, query, body, ...otherOptions } = {}) { + if (!headers) { + throw new InvalidArgumentError('Missing parameter: `headers`'); + } - if (!method) { - throw new InvalidArgumentError("Missing parameter: `method`"); - } + if (!method) { + throw new InvalidArgumentError('Missing parameter: `method`'); + } - if (!query) { - throw new InvalidArgumentError("Missing parameter: `query`"); - } + if (!query) { + throw new InvalidArgumentError('Missing parameter: `query`'); + } - this.body = body || {}; - this.headers = {}; - this.method = method; - this.query = query; + this.body = body || {}; + this.headers = {}; + this.method = method; + this.query = query; - // Store the headers in lower case. - Object.entries(headers).forEach(([header, value]) => { - this.headers[header.toLowerCase()] = value; - }); + // Store the headers in lower case. + Object.entries(headers).forEach(([header, value]) => { + this.headers[header.toLowerCase()] = value; + }); - // Store additional properties of the request object passed in - Object.entries(otherOptions) - .filter(([property]) => !this[property]) - .forEach(([property, value]) => { - this[property] = value; - }); - } + // Store additional properties of the request object passed in + Object.entries(otherOptions) + .filter(([property]) => !this[property]) + .forEach(([property, value]) => { + this[property] = value; + }); + } - /** - * Get a request header (case-insensitive). - * @param {String} field - * @return {string} - */ - get(field) { - return this.headers[field.toLowerCase()]; - } + /** + * Get a request header (case-insensitive). + * @param {String} field + * @return {string} + */ + get(field) { + return this.headers[field.toLowerCase()]; + } - /** - * Check if the content-type matches any of the given mime types. - * @param {...string[]} types - * @return {boolean} - */ - is(...types) { - return typeis(this, types.flat()) || false; - } + /** + * Check if the content-type matches any of the given mime types. + * @param {...string[]} types + * @return {boolean} + */ + is(...types) { + return typeis(this, types.flat()) || false; + } } module.exports = Request; diff --git a/lib/response-types/code-response-type.js b/lib/response-types/code-response-type.js index 0198bd59..d53770b7 100644 --- a/lib/response-types/code-response-type.js +++ b/lib/response-types/code-response-type.js @@ -1,47 +1,47 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const InvalidArgumentError = require("../errors/invalid-argument-error"); -const url = require("url"); +const InvalidArgumentError = require('../errors/invalid-argument-error'); +const url = require('url'); /** * @class * @classDesc */ class CodeResponseType { - /** - * @constructor - * @param code - * @throws {InvalidArgumentError} if {code} is missing - */ - constructor(code) { - if (!code) { - throw new InvalidArgumentError("Missing parameter: `code`"); - } - - this.code = code; - } - - /** - * @param redirectUri - * @return {UrlWithParsedQuery} - * @throws {InvalidArgumentError} if redirectUri is missing - */ - buildRedirectUri(redirectUri) { - if (!redirectUri) { - throw new InvalidArgumentError("Missing parameter: `redirectUri`"); - } - - const uri = url.parse(redirectUri, true); - - uri.query.code = this.code; - uri.search = null; - - return uri; - } + /** + * @constructor + * @param code + * @throws {InvalidArgumentError} if {code} is missing + */ + constructor(code) { + if (!code) { + throw new InvalidArgumentError('Missing parameter: `code`'); + } + + this.code = code; + } + + /** + * @param redirectUri + * @return {UrlWithParsedQuery} + * @throws {InvalidArgumentError} if redirectUri is missing + */ + buildRedirectUri(redirectUri) { + if (!redirectUri) { + throw new InvalidArgumentError('Missing parameter: `redirectUri`'); + } + + const uri = url.parse(redirectUri, true); + + uri.query.code = this.code; + uri.search = null; + + return uri; + } } module.exports = CodeResponseType; diff --git a/lib/response-types/token-response-type.js b/lib/response-types/token-response-type.js index f2ce2f99..95196717 100644 --- a/lib/response-types/token-response-type.js +++ b/lib/response-types/token-response-type.js @@ -1,23 +1,23 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const ServerError = require("../errors/server-error"); +const ServerError = require('../errors/server-error'); /** * @class * @classDesc */ class TokenResponseType { - /** - * @constructor - * @throws {ServerError} not implemented yet - */ - constructor() { - throw new ServerError("Not implemented."); - } + /** + * @constructor + * @throws {ServerError} not implemented yet + */ + constructor() { + throw new ServerError('Not implemented.'); + } } module.exports = TokenResponseType; diff --git a/lib/response.js b/lib/response.js index 8d8847d4..41a47136 100644 --- a/lib/response.js +++ b/lib/response.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; /** * Wrapper for webserver's response object. @@ -12,58 +12,58 @@ * } */ class Response { - /** - * Create a new Response instance. - * @constructor - * @param headers {object} key-value object of headers - * @param method {string} the HTTP method - * @param body {object=} optional key-value object of body parameters - * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver - */ - constructor({ headers = {}, body = {}, ...otherOptions } = {}) { - this.status = 200; - this.body = body; - this.headers = {}; + /** + * Create a new Response instance. + * @constructor + * @param headers {object} key-value object of headers + * @param method {string} the HTTP method + * @param body {object=} optional key-value object of body parameters + * @param otherOptions {...object} any other properties that should be assigned to the request by your webserver + */ + constructor({ headers = {}, body = {}, ...otherOptions } = {}) { + this.status = 200; + this.body = body; + this.headers = {}; - // Store the headers in lower case. - Object.entries(headers).forEach(([header, value]) => { - this.headers[header.toLowerCase()] = value; - }); + // Store the headers in lower case. + Object.entries(headers).forEach(([header, value]) => { + this.headers[header.toLowerCase()] = value; + }); - // Store additional properties of the response object passed in - Object.entries(otherOptions) - .filter(([property]) => !this[property]) - .forEach(([property, value]) => { - this[property] = value; - }); - } + // Store additional properties of the response object passed in + Object.entries(otherOptions) + .filter(([property]) => !this[property]) + .forEach(([property, value]) => { + this[property] = value; + }); + } - /** - * Get a response header. - * @param field {string} the field to access, case-insensitive - * @return {string|undefined} - */ - get(field) { - return this.headers[field.toLowerCase()]; - } + /** + * Get a response header. + * @param field {string} the field to access, case-insensitive + * @return {string|undefined} + */ + get(field) { + return this.headers[field.toLowerCase()]; + } - /** - * Redirect response. - * @param url {string} the url to redirect to - */ - redirect(url) { - this.set("Location", url); - this.status = 302; - } + /** + * Redirect response. + * @param url {string} the url to redirect to + */ + redirect(url) { + this.set('Location', url); + this.status = 302; + } - /** - * Set a response header. - * @param field {string} the name of the header field, case-insensitive - * @param value {string} the new value of the header field - */ - set(field, value) { - this.headers[field.toLowerCase()] = value; - } + /** + * Set a response header. + * @param field {string} the name of the header field, case-insensitive + * @param value {string} the new value of the header field + */ + set(field, value) { + this.headers[field.toLowerCase()] = value; + } } module.exports = Response; diff --git a/lib/server.js b/lib/server.js index b5800434..9afd409d 100644 --- a/lib/server.js +++ b/lib/server.js @@ -1,14 +1,14 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const AuthenticateHandler = require("./handlers/authenticate-handler"); -const AuthorizeHandler = require("./handlers/authorize-handler"); -const InvalidArgumentError = require("./errors/invalid-argument-error"); -const TokenHandler = require("./handlers/token-handler"); +const AuthenticateHandler = require('./handlers/authenticate-handler'); +const AuthorizeHandler = require('./handlers/authorize-handler'); +const InvalidArgumentError = require('./errors/invalid-argument-error'); +const TokenHandler = require('./handlers/token-handler'); // we require the model only for JSDoc linking -require("./model"); +require('./model'); /** * @class @@ -17,234 +17,234 @@ require("./model"); * const OAuth2Server = require('@node-oauth/oauth2-server'); */ class OAuth2Server { - /** - * Instantiates `OAuth2Server` using the supplied model. - * **Remarks:** - * - Any valid option for {@link OAuth2Server#authenticate}, {@link OAuth2Server#authorize} and {@link OAuth2Server#token} can be passed to the constructor as well. - * - The supplied options will be used as default for the other methods. - * - * @constructor - * @param options {object} Server options. - * @param options.model {Model} The Model; this is always required. - * - * @param options.scope {string[]|undefined} The scope(s) to authenticate. - * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. - * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. - * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. - * - * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). - * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user - * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state - * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) - * - * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). - * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). - * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). - * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. - * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. - * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. - * @param [options.enablePlainPKCE=false] {boolean} Allow the use of the `plain` code challenge method for PKCE. This is not recommended for production environments. - * - * @throws {InvalidArgumentError} if the model is missing - * @return {OAuth2Server} A new `OAuth2Server` instance. - * @example - * // Basic usage: - * const oauth = new OAuth2Server({ - * model: require('./model') - * }); - * @example - * // Advanced example with additional options: - * const oauth = new OAuth2Server({ - * model: require('./model'), - * allowBearerTokensInQueryString: true, - * accessTokenLifetime: 4 * 60 * 60 - * }); - */ - constructor(options) { - options = options || {}; + /** + * Instantiates `OAuth2Server` using the supplied model. + * **Remarks:** + * - Any valid option for {@link OAuth2Server#authenticate}, {@link OAuth2Server#authorize} and {@link OAuth2Server#token} can be passed to the constructor as well. + * - The supplied options will be used as default for the other methods. + * + * @constructor + * @param options {object} Server options. + * @param options.model {Model} The Model; this is always required. + * + * @param options.scope {string[]|undefined} The scope(s) to authenticate. + * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. + * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. + * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. + * + * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). + * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user + * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state + * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) + * + * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). + * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). + * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). + * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. + * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. + * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. + * @param [options.enablePlainPKCE=false] {boolean} Allow the use of the `plain` code challenge method for PKCE. This is not recommended for production environments. + * + * @throws {InvalidArgumentError} if the model is missing + * @return {OAuth2Server} A new `OAuth2Server` instance. + * @example + * // Basic usage: + * const oauth = new OAuth2Server({ + * model: require('./model') + * }); + * @example + * // Advanced example with additional options: + * const oauth = new OAuth2Server({ + * model: require('./model'), + * allowBearerTokensInQueryString: true, + * accessTokenLifetime: 4 * 60 * 60 + * }); + */ + constructor(options) { + options = options || {}; - if (!options.model) { - throw new InvalidArgumentError("Missing parameter: `model`"); - } + if (!options.model) { + throw new InvalidArgumentError('Missing parameter: `model`'); + } - this.options = options; - } + this.options = options; + } - /** - * Authenticates a request. - * @function - * @param options.scope {string[]|undefined} The scope(s) to authenticate. - * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. - * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. - * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. - * @throws {UnauthorizedRequestError} The protected resource request failed authentication. - * @return {Promise.} A `Promise` that resolves to the access token object returned from the model's `getAccessToken`. - * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. - * @example - * const oauth = new OAuth2Server({model: ...}); - * function authenticateHandler(options) { - * return function(req, res, next) { - * let request = new Request(req); - * let response = new Response(res); - * return oauth.authenticate(request, response, options) - * .then(function(token) { - * res.locals.oauth = {token: token}; - * next(); - * }) - * .catch(function(err) { - * // handle error condition - * }); - * } - * } - */ - authenticate(request, response, options) { - options = Object.assign( - { - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - allowBearerTokensInQueryString: false, - }, - this.options, - options, - ); + /** + * Authenticates a request. + * @function + * @param options.scope {string[]|undefined} The scope(s) to authenticate. + * @param [options.addAcceptedScopesHeader=true] {boolean=} Set the `X-Accepted-OAuth-Scopes` HTTP header on response objects. + * @param [options.addAuthorizedScopesHeader=true] {boolean=} Set the `X-OAuth-Scopes` HTTP header on response objects. + * @param [options.allowBearerTokensInQueryString=false] {boolean=} Allow clients to pass bearer tokens in the query string of a request. + * @throws {UnauthorizedRequestError} The protected resource request failed authentication. + * @return {Promise.} A `Promise` that resolves to the access token object returned from the model's `getAccessToken`. + * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. + * @example + * const oauth = new OAuth2Server({model: ...}); + * function authenticateHandler(options) { + * return function(req, res, next) { + * let request = new Request(req); + * let response = new Response(res); + * return oauth.authenticate(request, response, options) + * .then(function(token) { + * res.locals.oauth = {token: token}; + * next(); + * }) + * .catch(function(err) { + * // handle error condition + * }); + * } + * } + */ + authenticate(request, response, options) { + options = Object.assign( + { + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + allowBearerTokensInQueryString: false, + }, + this.options, + options, + ); - return new AuthenticateHandler(options).handle(request, response); - } + return new AuthenticateHandler(options).handle(request, response); + } - /** - * Authorizes a token request. - * **Remarks:** - * - * If `request.query.allowed` equals the string `'false'` the access request is denied and the returned promise is rejected with an `AccessDeniedError`. - * - * In order to retrieve the user associated with the request, `options.authenticateHandler` should be supplied. - * The `authenticateHandler` has to be an object implementing a `handle(request, response)` function that returns a user object. - * If there is no associated user (i.e. the user is not logged in) a falsy value should be returned. - * - * ```js - * let authenticateHandler = { - * handle: function(request, response) { - * return // get authenticated user; - * } - * }; - * ``` - * When working with a session-based login mechanism, the handler can simply look like this: - * ```js - * let authenticateHandler = { - * handle: function(request, response) { - * return request.session.user; - * } - * }; - * ``` - * - * @function - * @param request {Request} the Request instance object - * @param request.query.allowed {string=} `'false'` to deny the authorization request (see remarks section). - * @param response {Response} the Response instance object - * @param options {object=} handler options - * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). - * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user - * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state - * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) - * @throws {AccessDeniedError} The resource owner denied the access request (i.e. `request.query.allow` was `'false'`). - * @return {Promise.} A `Promise` that resolves to the authorization code object returned from model's `saveAuthorizationCode` - * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. - * @example - * const oauth = new OAuth2Server({model: ...}); - * function authorizeHandler(options) { - * return function(req, res, next) { - * let request = new Request(req); - * let response = new Response(res); - * return oauth.authorize(request, response, options) - * .then(function(code) { - * res.locals.oauth = {code: code}; - * next(); - * }) - * .catch(function(err) { - * // handle error condition - * }); - * } - * } - */ - authorize(request, response, options) { - options = Object.assign( - { - allowEmptyState: false, - authorizationCodeLifetime: 5 * 60, // 5 minutes. - }, - this.options, - options, - ); + /** + * Authorizes a token request. + * **Remarks:** + * + * If `request.query.allowed` equals the string `'false'` the access request is denied and the returned promise is rejected with an `AccessDeniedError`. + * + * In order to retrieve the user associated with the request, `options.authenticateHandler` should be supplied. + * The `authenticateHandler` has to be an object implementing a `handle(request, response)` function that returns a user object. + * If there is no associated user (i.e. the user is not logged in) a falsy value should be returned. + * + * ```js + * let authenticateHandler = { + * handle: function(request, response) { + * return // get authenticated user; + * } + * }; + * ``` + * When working with a session-based login mechanism, the handler can simply look like this: + * ```js + * let authenticateHandler = { + * handle: function(request, response) { + * return request.session.user; + * } + * }; + * ``` + * + * @function + * @param request {Request} the Request instance object + * @param request.query.allowed {string=} `'false'` to deny the authorization request (see remarks section). + * @param response {Response} the Response instance object + * @param options {object=} handler options + * @param options.authenticateHandler {object=} The authenticate handler (see remarks section). + * @param options.authenticateHandler.handle {function} The actual handler function to get an authenticated user + * @param [options.allowEmptyState=false] {boolean=} Allow clients to specify an empty `state + * @param [options.authorizationCodeLifetime=300] {number=} Lifetime of generated authorization codes in seconds (default = 300 s = 5 min) + * @throws {AccessDeniedError} The resource owner denied the access request (i.e. `request.query.allow` was `'false'`). + * @return {Promise.} A `Promise` that resolves to the authorization code object returned from model's `saveAuthorizationCode` + * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. + * @example + * const oauth = new OAuth2Server({model: ...}); + * function authorizeHandler(options) { + * return function(req, res, next) { + * let request = new Request(req); + * let response = new Response(res); + * return oauth.authorize(request, response, options) + * .then(function(code) { + * res.locals.oauth = {code: code}; + * next(); + * }) + * .catch(function(err) { + * // handle error condition + * }); + * } + * } + */ + authorize(request, response, options) { + options = Object.assign( + { + allowEmptyState: false, + authorizationCodeLifetime: 5 * 60, // 5 minutes. + }, + this.options, + options, + ); - return new AuthorizeHandler(options).handle(request, response); - } + return new AuthorizeHandler(options).handle(request, response); + } - /** - * Retrieves a new token for an authorized token request. - * **Remarks:** - * If `options.allowExtendedTokenAttributes` is `true` any additional properties set on the object returned from `Model#saveToken() ` are copied to the token response sent to the client. - * By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. - * ```js - * let options = { - * // ... - * // Allow token requests using the password grant to not include a client_secret. - * requireClientAuthentication: {password: false} - * }; - * ``` - * `options.extendedGrantTypes` is an object mapping extension grant URIs to handler types, for example: - * ```js - * let options = { - * // ... - * extendedGrantTypes: { - * 'urn:foo:bar:baz': MyGrantType - * } - * }; - * ``` - * For information on how to implement a handler for a custom grant type see the extension grants. - * @function - * @param request {Request} the Request instance object - * @param response {Response} the Response instance object - * @param options {object=} handler options - * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). - * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). - * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). - * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. - * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. - * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. - * @return {Promise.} A `Promise` that resolves to the token object returned from the model's `saveToken` method. - * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. - * @throws {InvalidGrantError} The access token request was invalid or not authorized. - * @example - * const oauth = new OAuth2Server({model: ...}); - * function tokenHandler(options) { - * return function(req, res, next) { - * let request = new Request(req); - * let response = new Response(res); - * return oauth.token(request, response, options) - * .then(function(code) { - * res.locals.oauth = {token: token}; - * next(); - * }) - * .catch(function(err) { - * // handle error condition - * }); - * } - * } - */ - token(request, response, options) { - options = Object.assign( - { - accessTokenLifetime: 60 * 60, // 1 hour. - refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks. - allowExtendedTokenAttributes: false, - requireClientAuthentication: {}, // defaults to true for all grant types - }, - this.options, - options, - ); + /** + * Retrieves a new token for an authorized token request. + * **Remarks:** + * If `options.allowExtendedTokenAttributes` is `true` any additional properties set on the object returned from `Model#saveToken() ` are copied to the token response sent to the client. + * By default, all grant types require the client to send it's `client_secret` with the token request. `options.requireClientAuthentication` can be used to disable this check for selected grants. If used, this server option must be an object containing properties set to `true` or `false`. Possible keys for the object include all supported values for the token request's `grant_type` field (`authorization_code`, `client_credentials`, `password` and `refresh_token`). Grants that are not specified default to `true` which enables verification of the `client_secret`. + * ```js + * let options = { + * // ... + * // Allow token requests using the password grant to not include a client_secret. + * requireClientAuthentication: {password: false} + * }; + * ``` + * `options.extendedGrantTypes` is an object mapping extension grant URIs to handler types, for example: + * ```js + * let options = { + * // ... + * extendedGrantTypes: { + * 'urn:foo:bar:baz': MyGrantType + * } + * }; + * ``` + * For information on how to implement a handler for a custom grant type see the extension grants. + * @function + * @param request {Request} the Request instance object + * @param response {Response} the Response instance object + * @param options {object=} handler options + * @param [options.accessTokenLifetime=3600] {number=} Lifetime of generated access tokens in seconds (default = 1 hour). + * @param [options.refreshTokenLifetime=1209600] {number=} Lifetime of generated refresh tokens in seconds (default = 2 weeks). + * @param [options.allowExtendedTokenAttributes=false] {boolean=} Allow extended attributes to be set on the returned token (see remarks section). + * @param [options.requireClientAuthentication=object] {object|boolean} Require a client secret for grant types (names as keys). Defaults to `true` for all grant types. + * @param [options.alwaysIssueNewRefreshToken=true] {boolean=} Always revoke the used refresh token and issue a new one for the `refresh_token` grant. + * @param [options.extendedGrantTypes=object] {object} Additional supported grant types. + * @return {Promise.} A `Promise` that resolves to the token object returned from the model's `saveToken` method. + * In case of an error, the promise rejects with one of the error types derived from `OAuthError`. + * @throws {InvalidGrantError} The access token request was invalid or not authorized. + * @example + * const oauth = new OAuth2Server({model: ...}); + * function tokenHandler(options) { + * return function(req, res, next) { + * let request = new Request(req); + * let response = new Response(res); + * return oauth.token(request, response, options) + * .then(function(code) { + * res.locals.oauth = {token: token}; + * next(); + * }) + * .catch(function(err) { + * // handle error condition + * }); + * } + * } + */ + token(request, response, options) { + options = Object.assign( + { + accessTokenLifetime: 60 * 60, // 1 hour. + refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks. + allowExtendedTokenAttributes: false, + requireClientAuthentication: {}, // defaults to true for all grant types + }, + this.options, + options, + ); - return new TokenHandler(options).handle(request, response); - } + return new TokenHandler(options).handle(request, response); + } } module.exports = OAuth2Server; diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js index f76fde37..fe76b0d7 100644 --- a/lib/token-types/bearer-token-type.js +++ b/lib/token-types/bearer-token-type.js @@ -1,74 +1,74 @@ -"use strict"; +'use strict'; /* * Module dependencies. */ -const InvalidArgumentError = require("../errors/invalid-argument-error"); +const InvalidArgumentError = require('../errors/invalid-argument-error'); /** * @class * @classDesc */ class BearerTokenType { - /** - * @constructor - * @param accessToken - * @param accessTokenLifetime - * @param refreshToken - * @param scope - * @param customAttributes - */ - constructor( - accessToken, - accessTokenLifetime, - refreshToken, - scope, - customAttributes, - ) { - if (!accessToken) { - throw new InvalidArgumentError("Missing parameter: `accessToken`"); - } + /** + * @constructor + * @param accessToken + * @param accessTokenLifetime + * @param refreshToken + * @param scope + * @param customAttributes + */ + constructor( + accessToken, + accessTokenLifetime, + refreshToken, + scope, + customAttributes, + ) { + if (!accessToken) { + throw new InvalidArgumentError('Missing parameter: `accessToken`'); + } - this.accessToken = accessToken; - this.accessTokenLifetime = accessTokenLifetime; - this.refreshToken = refreshToken; - this.scope = scope; + this.accessToken = accessToken; + this.accessTokenLifetime = accessTokenLifetime; + this.refreshToken = refreshToken; + this.scope = scope; - if (customAttributes) { - this.customAttributes = customAttributes; - } - } + if (customAttributes) { + this.customAttributes = customAttributes; + } + } - /** - * Retrieve the value representation. - */ + /** + * Retrieve the value representation. + */ - valueOf() { - const object = { - access_token: this.accessToken, - token_type: "Bearer", - }; + valueOf() { + const object = { + access_token: this.accessToken, + token_type: 'Bearer', + }; - if (this.accessTokenLifetime) { - object.expires_in = this.accessTokenLifetime; - } + if (this.accessTokenLifetime) { + object.expires_in = this.accessTokenLifetime; + } - if (this.refreshToken) { - object.refresh_token = this.refreshToken; - } + if (this.refreshToken) { + object.refresh_token = this.refreshToken; + } - if (this.scope) { - object.scope = this.scope; - } + if (this.scope) { + object.scope = this.scope; + } - for (const key in this.customAttributes) { - if (Object.prototype.hasOwnProperty.call(this.customAttributes, key)) { - object[key] = this.customAttributes[key]; - } - } - return object; - } + for (const key in this.customAttributes) { + if (Object.prototype.hasOwnProperty.call(this.customAttributes, key)) { + object[key] = this.customAttributes[key]; + } + } + return object; + } } module.exports = BearerTokenType; diff --git a/lib/token-types/mac-token-type.js b/lib/token-types/mac-token-type.js index 44274aa4..ce620c8b 100644 --- a/lib/token-types/mac-token-type.js +++ b/lib/token-types/mac-token-type.js @@ -1,19 +1,19 @@ -"use strict"; +'use strict'; -const ServerError = require("../errors/server-error"); +const ServerError = require('../errors/server-error'); /** * @class * @classDesc */ class MacTokenType { - /** - * @constructor - * @throws {ServerError} not yet implemented - */ - constructor() { - throw new ServerError("Not implemented."); - } + /** + * @constructor + * @throws {ServerError} not yet implemented + */ + constructor() { + throw new ServerError('Not implemented.'); + } } module.exports = MacTokenType; diff --git a/lib/utils/crypto-util.js b/lib/utils/crypto-util.js index 4780c81d..6a3f7104 100644 --- a/lib/utils/crypto-util.js +++ b/lib/utils/crypto-util.js @@ -1,6 +1,6 @@ -"use strict"; +'use strict'; -const crypto = require("crypto"); +const crypto = require('crypto'); /** * @module CryptoUtil @@ -17,8 +17,8 @@ const crypto = require("crypto"); * @param output {'base64'|'base64url'|'binary'|'hex'|undefined} optional, the desired output type * @return {Buffer|string} if {output} is undefined, a {Buffer} is returned, otherwise a {String} */ -const createHash = function ({ algorithm = "sha256", data, output, encoding }) { - return crypto.createHash(algorithm).update(data, encoding).digest(output); +const createHash = function ({ algorithm = 'sha256', data, output, encoding }) { + return crypto.createHash(algorithm).update(data, encoding).digest(output); }; module.exports = { createHash }; diff --git a/lib/utils/date-util.js b/lib/utils/date-util.js index ae8983c7..4fe32f97 100644 --- a/lib/utils/date-util.js +++ b/lib/utils/date-util.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; /** * @module DateUtil @@ -12,9 +12,9 @@ * @return {number} The number of seconds until the expiration date. */ function getLifetimeFromExpiresAt(expiresAt) { - return Math.floor((expiresAt - new Date()) / 1000); + return Math.floor((expiresAt - new Date()) / 1000); } module.exports = { - getLifetimeFromExpiresAt, + getLifetimeFromExpiresAt, }; diff --git a/lib/utils/scope-util.js b/lib/utils/scope-util.js index 4c01cda1..4cf98dff 100644 --- a/lib/utils/scope-util.js +++ b/lib/utils/scope-util.js @@ -1,5 +1,5 @@ -const isFormat = require("@node-oauth/formats"); -const InvalidScopeError = require("../errors/invalid-scope-error"); +const isFormat = require('@node-oauth/formats'); +const InvalidScopeError = require('../errors/invalid-scope-error'); const whiteSpace = /\s+/g; /** @@ -18,23 +18,23 @@ const whiteSpace = /\s+/g; * @see {https://github.com/node-oauth/formats} */ function parseScope(requestedScope) { - if (requestedScope == null) { - return undefined; - } + if (requestedScope == null) { + return undefined; + } - if (typeof requestedScope !== "string") { - throw new InvalidScopeError("Invalid parameter: `scope`"); - } + if (typeof requestedScope !== 'string') { + throw new InvalidScopeError('Invalid parameter: `scope`'); + } - // XXX: this prevents spaced-only strings to become - // treated as valid nqchar by making them empty strings - requestedScope = requestedScope.trim(); + // XXX: this prevents spaced-only strings to become + // treated as valid nqchar by making them empty strings + requestedScope = requestedScope.trim(); - if (!isFormat.nqschar(requestedScope)) { - throw new InvalidScopeError("Invalid parameter: `scope`"); - } + if (!isFormat.nqschar(requestedScope)) { + throw new InvalidScopeError('Invalid parameter: `scope`'); + } - return requestedScope.split(whiteSpace); + return requestedScope.split(whiteSpace); } module.exports = { parseScope }; diff --git a/lib/utils/string-util.js b/lib/utils/string-util.js index 7dad0e9d..cc514e08 100644 --- a/lib/utils/string-util.js +++ b/lib/utils/string-util.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; /** * @module StringUtil @@ -11,11 +11,11 @@ * @return {string} */ function base64URLEncode(str) { - return str - .toString("base64") - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=/g, ""); + return str + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); } module.exports = { base64URLEncode }; diff --git a/lib/utils/token-util.js b/lib/utils/token-util.js index a2f793ac..06eec35d 100644 --- a/lib/utils/token-util.js +++ b/lib/utils/token-util.js @@ -1,6 +1,6 @@ -"use strict"; +'use strict'; -const randomBytes = require("crypto").randomBytes; +const randomBytes = require('crypto').randomBytes; /** * @module TokenUtil @@ -13,17 +13,17 @@ const randomBytes = require("crypto").randomBytes; * @return {Promise} */ function generateRandomToken() { - return new Promise((resolve, reject) => { - randomBytes(32, (err, data) => { - if (err) { - reject(err); - } else { - resolve(data.toString("hex")); - } - }); - }); + return new Promise((resolve, reject) => { + randomBytes(32, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data.toString('hex')); + } + }); + }); } module.exports = { - generateRandomToken, + generateRandomToken, }; diff --git a/test/assertions.js b/test/assertions.js index 0d9691b8..7a95c809 100644 --- a/test/assertions.js +++ b/test/assertions.js @@ -1,18 +1,18 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const chai = require("chai"); +const chai = require('chai'); /** * SHA-256 assertion. */ chai.use(function (_chai, utils) { - chai.Assertion.addMethod("sha256", function (...args) { - const obj = utils.flag(this, "object"); - new chai.Assertion(obj).match(/^[a-f0-9]{64}$/i); - }); + chai.Assertion.addMethod('sha256', function (...args) { + const obj = utils.flag(this, 'object'); + new chai.Assertion(obj).match(/^[a-f0-9]{64}$/i); + }); }); diff --git a/test/compliance/client-authentication_test.js b/test/compliance/client-authentication_test.js index 8750637d..2bd299d0 100644 --- a/test/compliance/client-authentication_test.js +++ b/test/compliance/client-authentication_test.js @@ -18,117 +18,117 @@ * parameter if the client secret is an empty string. */ -const OAuth2Server = require("../.."); -const DB = require("../helpers/db"); -const createModel = require("../helpers/model"); -const createRequest = require("../helpers/request"); -const Response = require("../../lib/response"); +const OAuth2Server = require('../..'); +const DB = require('../helpers/db'); +const createModel = require('../helpers/model'); +const createRequest = require('../helpers/request'); +const Response = require('../../lib/response'); -require("chai").should(); +require('chai').should(); const db = new DB(); const auth = new OAuth2Server({ - model: createModel(db), + model: createModel(db), }); -const user = db.saveUser({ id: 1, username: "test", password: "test" }); -const client = db.saveClient({ id: "a", secret: "b", grants: ["password"] }); -const scope = "read write"; +const user = db.saveUser({ id: 1, username: 'test', password: 'test' }); +const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password'] }); +const scope = 'read write'; function createDefaultRequest() { - return createRequest({ - body: { - grant_type: "password", - username: user.username, - password: user.password, - scope, - }, - headers: { - authorization: - "Basic " + - Buffer.from(client.id + ":" + client.secret).toString("base64"), - "content-type": "application/x-www-form-urlencoded", - }, - method: "POST", - }); + return createRequest({ + body: { + grant_type: 'password', + username: user.username, + password: user.password, + scope, + }, + headers: { + authorization: + 'Basic ' + + Buffer.from(client.id + ':' + client.secret).toString('base64'), + 'content-type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + }); } -describe("Client Authentication Compliance", function () { - describe("No authentication", function () { - it("should be an unsuccesfull authentication", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.headers.authorization; - - await auth - .token(request, response, {}) - .then((token) => { - throw new Error("Should not be here"); - }) - .catch((err) => { - err.name.should.equal("invalid_client"); - }); - }); - }); - - describe("Basic Authentication", function () { - it("should be a succesfull authentication", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - await auth.token(request, response, {}); - }); - - it("should be an unsuccesfull authentication", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - request.headers.authorization = - "Basic " + Buffer.from("a:c").toString("base64"); - - await auth - .token(request, response, {}) - .then((token) => { - throw new Error("Should not be here"); - }) - .catch((err) => { - err.name.should.equal("invalid_client"); - }); - }); - }); - - describe("Request body authentication", function () { - it("should be a succesfull authentication", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.headers.authorization; - - request.body.client_id = client.id; - request.body.client_secret = client.secret; - - await auth.token(request, response, {}); - }); - - it("should be an unsuccesfull authentication", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.headers.authorization; - - request.body.client_id = "a"; - request.body.client_secret = "c"; - - await auth - .token(request, response, {}) - .then((token) => { - throw new Error("Should not be here"); - }) - .catch((err) => { - err.name.should.equal("invalid_client"); - }); - }); - }); +describe('Client Authentication Compliance', function () { + describe('No authentication', function () { + it('should be an unsuccesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.headers.authorization; + + await auth + .token(request, response, {}) + .then((token) => { + throw new Error('Should not be here'); + }) + .catch((err) => { + err.name.should.equal('invalid_client'); + }); + }); + }); + + describe('Basic Authentication', function () { + it('should be a succesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + await auth.token(request, response, {}); + }); + + it('should be an unsuccesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.headers.authorization = + 'Basic ' + Buffer.from('a:c').toString('base64'); + + await auth + .token(request, response, {}) + .then((token) => { + throw new Error('Should not be here'); + }) + .catch((err) => { + err.name.should.equal('invalid_client'); + }); + }); + }); + + describe('Request body authentication', function () { + it('should be a succesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.headers.authorization; + + request.body.client_id = client.id; + request.body.client_secret = client.secret; + + await auth.token(request, response, {}); + }); + + it('should be an unsuccesfull authentication', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.headers.authorization; + + request.body.client_id = 'a'; + request.body.client_secret = 'c'; + + await auth + .token(request, response, {}) + .then((token) => { + throw new Error('Should not be here'); + }) + .catch((err) => { + err.name.should.equal('invalid_client'); + }); + }); + }); }); diff --git a/test/compliance/client-credential-workflow_test.js b/test/compliance/client-credential-workflow_test.js index 080ab412..154df0a1 100644 --- a/test/compliance/client-credential-workflow_test.js +++ b/test/compliance/client-credential-workflow_test.js @@ -14,136 +14,136 @@ * @see https://www.rfc-editor.org/rfc/rfc6749#section-4.4 */ -const OAuth2Server = require("../.."); -const DB = require("../helpers/db"); -const createModel = require("../helpers/model"); -const createRequest = require("../helpers/request"); -const Response = require("../../lib/response"); +const OAuth2Server = require('../..'); +const DB = require('../helpers/db'); +const createModel = require('../helpers/model'); +const createRequest = require('../helpers/request'); +const Response = require('../../lib/response'); -require("chai").should(); +require('chai').should(); const db = new DB(); // this user represents requests in the name of an external server // TODO: we should discuss, if we can make user optional for client credential workflows // as it's not desired to have an extra fake-user representing a server just to pass validation -const userDoc = { id: "machine2-123456789", name: "machine2" }; +const userDoc = { id: 'machine2-123456789', name: 'machine2' }; db.saveUser(userDoc); const oAuth2Server = new OAuth2Server({ - model: { - ...createModel(db), - getUserFromClient: async function (_client) { - // in a machine2machine setup we might not have a dedicated "user" - // but we need to return a truthy response to - const client = db.findClient(_client.id, _client.secret); - return client && { ...userDoc }; - }, - }, + model: { + ...createModel(db), + getUserFromClient: async function (_client) { + // in a machine2machine setup we might not have a dedicated "user" + // but we need to return a truthy response to + const client = db.findClient(_client.id, _client.secret); + return client && { ...userDoc }; + }, + }, }); const clientDoc = db.saveClient({ - id: "client-credential-test-client", - secret: "client-credential-test-secret", - grants: ["client_credentials"], + id: 'client-credential-test-client', + secret: 'client-credential-test-secret', + grants: ['client_credentials'], }); -const enabledScope = "read write"; +const enabledScope = 'read write'; -describe("ClientCredentials Workflow Compliance (4.4)", function () { - describe("Access Token Request (4.4.1)", function () { - /** - * 4.4.2. Access Token Request - * - * The client makes a request to the token endpoint by adding the - * following parameters using the "application/x-www-form-urlencoded" - * format per Appendix B with a character encoding of UTF-8 in the HTTP - * request entity-body: - * - * grant_type - * REQUIRED. Value MUST be set to "client_credentials". - * - * scope - * OPTIONAL. The scope of the access request as described by - * Section 3.3. - * - * The client MUST authenticate with the authorization server as - * described in Section 3.2.1. - */ - it("authenticates the client with valid credentials", async function () { - const response = new Response(); - const request = createRequest({ - body: { - grant_type: "client_credentials", - scope: enabledScope, - }, - headers: { - authorization: - "Basic " + - Buffer.from(clientDoc.id + ":" + clientDoc.secret).toString( - "base64", - ), - "content-type": "application/x-www-form-urlencoded", - }, - method: "POST", - }); +describe('ClientCredentials Workflow Compliance (4.4)', function () { + describe('Access Token Request (4.4.1)', function () { + /** + * 4.4.2. Access Token Request + * + * The client makes a request to the token endpoint by adding the + * following parameters using the "application/x-www-form-urlencoded" + * format per Appendix B with a character encoding of UTF-8 in the HTTP + * request entity-body: + * + * grant_type + * REQUIRED. Value MUST be set to "client_credentials". + * + * scope + * OPTIONAL. The scope of the access request as described by + * Section 3.3. + * + * The client MUST authenticate with the authorization server as + * described in Section 3.2.1. + */ + it('authenticates the client with valid credentials', async function () { + const response = new Response(); + const request = createRequest({ + body: { + grant_type: 'client_credentials', + scope: enabledScope, + }, + headers: { + authorization: + 'Basic ' + + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString( + 'base64', + ), + 'content-type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + }); - const token = await oAuth2Server.token(request, response); + const token = await oAuth2Server.token(request, response); - response.status.should.equal(200); - response.headers.should.deep.equal({ - "cache-control": "no-store", - pragma: "no-cache", - }); - response.body.token_type.should.equal("Bearer"); - response.body.access_token.should.equal(token.accessToken); - response.body.expires_in.should.be.a("number"); - response.body.scope.should.eql("read write"); - ("refresh_token" in response.body).should.equal(false); + response.status.should.equal(200); + response.headers.should.deep.equal({ + 'cache-control': 'no-store', + pragma: 'no-cache', + }); + response.body.token_type.should.equal('Bearer'); + response.body.access_token.should.equal(token.accessToken); + response.body.expires_in.should.be.a('number'); + response.body.scope.should.eql('read write'); + ('refresh_token' in response.body).should.equal(false); - token.accessToken.should.be.a("string"); - token.accessTokenExpiresAt.should.be.a("date"); - ("refreshToken" in token).should.equal(false); - ("refreshTokenExpiresAt" in token).should.equal(false); - token.scope.should.eql(["read", "write"]); + token.accessToken.should.be.a('string'); + token.accessTokenExpiresAt.should.be.a('date'); + ('refreshToken' in token).should.equal(false); + ('refreshTokenExpiresAt' in token).should.equal(false); + token.scope.should.eql(['read', 'write']); - db.accessTokens.has(token.accessToken).should.equal(true); - db.refreshTokens.has(token.refreshToken).should.equal(false); - }); + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(false); + }); - /** - * 7. Accessing Protected Resources - * - * The client accesses protected resources by presenting the access - * token to the resource server. The resource server MUST validate the - * access token and ensure that it has not expired and that its scope - * covers the requested resource. The methods used by the resource - * server to validate the access token (as well as any error responses) - * are beyond the scope of this specification but generally involve an - * interaction or coordination between the resource server and the - * authorization server. - */ - it("enables an authenticated request using the access token", async function () { - const [accessToken] = [...db.accessTokens.entries()][0]; - const response = new Response(); - const request = createRequest({ - query: {}, - headers: { - authorization: `Bearer ${accessToken}`, - }, - method: "GET", - }); + /** + * 7. Accessing Protected Resources + * + * The client accesses protected resources by presenting the access + * token to the resource server. The resource server MUST validate the + * access token and ensure that it has not expired and that its scope + * covers the requested resource. The methods used by the resource + * server to validate the access token (as well as any error responses) + * are beyond the scope of this specification but generally involve an + * interaction or coordination between the resource server and the + * authorization server. + */ + it('enables an authenticated request using the access token', async function () { + const [accessToken] = [...db.accessTokens.entries()][0]; + const response = new Response(); + const request = createRequest({ + query: {}, + headers: { + authorization: `Bearer ${accessToken}`, + }, + method: 'GET', + }); - const token = await oAuth2Server.authenticate(request, response); - token.accessToken.should.equal(accessToken); - token.user.should.deep.equal(userDoc); - token.client.should.deep.equal(clientDoc); - token.scope.should.eql(["read", "write"]); + const token = await oAuth2Server.authenticate(request, response); + token.accessToken.should.equal(accessToken); + token.user.should.deep.equal(userDoc); + token.client.should.deep.equal(clientDoc); + token.scope.should.eql(['read', 'write']); - response.status.should.equal(200); - // there should be no information in the response as it - // should only add information, if permission is denied - response.body.should.deep.equal({}); - response.headers.should.deep.equal({}); - }); - }); + response.status.should.equal(200); + // there should be no information in the response as it + // should only add information, if permission is denied + response.body.should.deep.equal({}); + response.headers.should.deep.equal({}); + }); + }); }); diff --git a/test/compliance/password-grant-type_test.js b/test/compliance/password-grant-type_test.js index f8a42207..2596ae1f 100644 --- a/test/compliance/password-grant-type_test.js +++ b/test/compliance/password-grant-type_test.js @@ -55,183 +55,183 @@ * developer with additional information about the error. */ -const OAuth2Server = require("../.."); -const DB = require("../helpers/db"); -const createModel = require("../helpers/model"); -const createRequest = require("../helpers/request"); -const Response = require("../../lib/response"); -const crypto = require("crypto"); +const OAuth2Server = require('../..'); +const DB = require('../helpers/db'); +const createModel = require('../helpers/model'); +const createRequest = require('../helpers/request'); +const Response = require('../../lib/response'); +const crypto = require('crypto'); -require("chai").should(); +require('chai').should(); const db = new DB(); const auth = new OAuth2Server({ - model: createModel(db), + model: createModel(db), }); -const user = db.saveUser({ id: 1, username: "test", password: "test" }); -const client = db.saveClient({ id: "a", secret: "b", grants: ["password"] }); -const scope = "read write"; +const user = db.saveUser({ id: 1, username: 'test', password: 'test' }); +const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password'] }); +const scope = 'read write'; function createDefaultRequest() { - return createRequest({ - body: { - grant_type: "password", - username: user.username, - password: user.password, - scope, - }, - headers: { - authorization: - "Basic " + - Buffer.from(client.id + ":" + client.secret).toString("base64"), - "content-type": "application/x-www-form-urlencoded", - }, - method: "POST", - }); + return createRequest({ + body: { + grant_type: 'password', + username: user.username, + password: user.password, + scope, + }, + headers: { + authorization: + 'Basic ' + + Buffer.from(client.id + ':' + client.secret).toString('base64'), + 'content-type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + }); } -describe("PasswordGrantType Compliance", function () { - describe("Authenticate", function () { - it("Succesfull authorization", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - const token = await auth.token(request, response, {}); - response.body.token_type.should.equal("Bearer"); - response.body.access_token.should.equal(token.accessToken); - response.body.refresh_token.should.equal(token.refreshToken); - response.body.expires_in.should.be.a("number"); - response.body.scope.should.eql("read write"); - - token.accessToken.should.be.a("string"); - token.refreshToken.should.be.a("string"); - token.accessTokenExpiresAt.should.be.a("date"); - token.refreshTokenExpiresAt.should.be.a("date"); - token.scope.should.eql(["read", "write"]); - - db.accessTokens.has(token.accessToken).should.equal(true); - db.refreshTokens.has(token.refreshToken).should.equal(true); - }); - - it("Succesfull authorization and authentication", async function () { - const tokenRequest = createDefaultRequest(); - const tokenResponse = new Response({}); - - const token = await auth.token(tokenRequest, tokenResponse, {}); - - const authenticationRequest = createRequest({ - body: {}, - headers: { - Authorization: `Bearer ${token.accessToken}`, - }, - method: "GET", - query: {}, - }); - const authenticationResponse = new Response({}); - - const authenticated = await auth.authenticate( - authenticationRequest, - authenticationResponse, - {}, - ); - - authenticated.scope.should.eql(["read", "write"]); - authenticated.user.should.be.an("object"); - authenticated.client.should.be.an("object"); - }); - - it("Username missing", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.body.username; - - await auth.token(request, response, {}).catch((err) => { - err.name.should.equal("invalid_request"); - }); - }); - - it("Password missing", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.body.password; - - await auth.token(request, response, {}).catch((err) => { - err.name.should.equal("invalid_request"); - }); - }); - - it("Wrong username", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - request.body.username = "wrong"; - - await auth.token(request, response, {}).catch((err) => { - err.name.should.equal("invalid_grant"); - }); - }); - - it("Wrong password", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - request.body.password = "wrong"; - - await auth.token(request, response, {}).catch((err) => { - err.name.should.equal("invalid_grant"); - }); - }); - - it("Client not found", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - const clientId = crypto.randomBytes(4).toString("hex"); - const clientSecret = crypto.randomBytes(4).toString("hex"); - - request.headers.authorization = - "Basic " + - Buffer.from(`${clientId}:${clientSecret}`).toString("base64"); - - await auth.token(request, response, {}).catch((err) => { - err.name.should.equal("invalid_client"); - }); - }); - - it("Client secret not required", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.body.client_secret; - - const token = await auth.token(request, response, { - requireClientAuthentication: { - password: false, - }, - }); - - token.accessToken.should.be.a("string"); - }); - - it("Client secret required", async function () { - const request = createDefaultRequest(); - const response = new Response({}); - - delete request.body.client_secret; - - await auth - .token(request, response, { - requireClientAuthentication: { - password: false, - }, - }) - .catch((err) => { - err.name.should.equal("invalid_client"); - }); - }); - }); +describe('PasswordGrantType Compliance', function () { + describe('Authenticate', function () { + it('Succesfull authorization', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + const token = await auth.token(request, response, {}); + response.body.token_type.should.equal('Bearer'); + response.body.access_token.should.equal(token.accessToken); + response.body.refresh_token.should.equal(token.refreshToken); + response.body.expires_in.should.be.a('number'); + response.body.scope.should.eql('read write'); + + token.accessToken.should.be.a('string'); + token.refreshToken.should.be.a('string'); + token.accessTokenExpiresAt.should.be.a('date'); + token.refreshTokenExpiresAt.should.be.a('date'); + token.scope.should.eql(['read', 'write']); + + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(true); + }); + + it('Succesfull authorization and authentication', async function () { + const tokenRequest = createDefaultRequest(); + const tokenResponse = new Response({}); + + const token = await auth.token(tokenRequest, tokenResponse, {}); + + const authenticationRequest = createRequest({ + body: {}, + headers: { + Authorization: `Bearer ${token.accessToken}`, + }, + method: 'GET', + query: {}, + }); + const authenticationResponse = new Response({}); + + const authenticated = await auth.authenticate( + authenticationRequest, + authenticationResponse, + {}, + ); + + authenticated.scope.should.eql(['read', 'write']); + authenticated.user.should.be.an('object'); + authenticated.client.should.be.an('object'); + }); + + it('Username missing', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.username; + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal('invalid_request'); + }); + }); + + it('Password missing', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.password; + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal('invalid_request'); + }); + }); + + it('Wrong username', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.body.username = 'wrong'; + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal('invalid_grant'); + }); + }); + + it('Wrong password', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + request.body.password = 'wrong'; + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal('invalid_grant'); + }); + }); + + it('Client not found', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + const clientId = crypto.randomBytes(4).toString('hex'); + const clientSecret = crypto.randomBytes(4).toString('hex'); + + request.headers.authorization = + 'Basic ' + + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); + + await auth.token(request, response, {}).catch((err) => { + err.name.should.equal('invalid_client'); + }); + }); + + it('Client secret not required', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.client_secret; + + const token = await auth.token(request, response, { + requireClientAuthentication: { + password: false, + }, + }); + + token.accessToken.should.be.a('string'); + }); + + it('Client secret required', async function () { + const request = createDefaultRequest(); + const response = new Response({}); + + delete request.body.client_secret; + + await auth + .token(request, response, { + requireClientAuthentication: { + password: false, + }, + }) + .catch((err) => { + err.name.should.equal('invalid_client'); + }); + }); + }); }); diff --git a/test/compliance/pkce_test.js b/test/compliance/pkce_test.js index b3ea63e2..24f6b7cd 100644 --- a/test/compliance/pkce_test.js +++ b/test/compliance/pkce_test.js @@ -36,661 +36,661 @@ * violate RFC 7636 itself. */ -const OAuth2Server = require("../.."); -const DB = require("../helpers/db"); -const createModel = require("../helpers/model"); -const createRequest = require("../helpers/request"); -const Response = require("../../lib/response"); -const { base64URLEncode } = require("../../lib/utils/string-util"); -const { createHash } = require("../../lib/utils/crypto-util"); -const { InvalidRequestError } = require("../../index"); -const ServerError = require("../../lib/errors/server-error"); -const InvalidGrantError = require("../../lib/errors/invalid-grant-error"); -require("chai").should(); +const OAuth2Server = require('../..'); +const DB = require('../helpers/db'); +const createModel = require('../helpers/model'); +const createRequest = require('../helpers/request'); +const Response = require('../../lib/response'); +const { base64URLEncode } = require('../../lib/utils/string-util'); +const { createHash } = require('../../lib/utils/crypto-util'); +const { InvalidRequestError } = require('../../index'); +const ServerError = require('../../lib/errors/server-error'); +const InvalidGrantError = require('../../lib/errors/invalid-grant-error'); +require('chai').should(); /** * Compute the S256 code_challenge for a given verifier, * using the same logic the server uses internally. */ function computeS256Challenge(verifier) { - const hash = createHash({ data: verifier }); - return base64URLEncode(hash); + const hash = createHash({ data: verifier }); + return base64URLEncode(hash); } -describe("PKCE Compliance (RFC 7636)", function () { - // --------------------------------------------------------------- - // Shared fixtures - // --------------------------------------------------------------- - let db, oAuth2Server; - - const userDoc = { - id: "pkce-user-1", - username: "pkceuser", - password: "pkcepass", - }; - const clientDoc = { - id: "pkce-client", - secret: "pkce-secret", - grants: ["authorization_code"], - redirectUris: ["https://client.example/callback"], - }; - - /** - * Helper: seed a fresh authorization code into the DB that carries - * a PKCE code challenge (S256). - * @param {string} verifier The code_verifier to use for the code_challenge. Should be a valid string but can be weak (e.g. 1 char) to demonstrate vulnerabilities. - * @param {string} codeValue Optional code value to use (for testing). If not provided, a random one will be generated. - * @param {string} method Optional code_challenge_method to use (default "S256"). For testing the "plain" method, set this to "plain" and ensure the verifier is the same as the challenge. - * @return {object} The authorization code document that was seeded into the DB. - */ - function seedAuthorizationCode(verifier, codeValue, method = "S256") { - codeValue = codeValue || "auth-code-" + Math.random().toString(36).slice(2); - const codeChallenge = computeS256Challenge(verifier); - const codeDoc = { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), // 1 min from now - redirectUri: "https://client.example/callback", - client: clientDoc, - user: userDoc, - scope: ["read"], - codeChallenge, - codeChallengeMethod: method, - }; - // store in DB so getAuthorizationCode can find it - db.authorizationCodes.set(codeValue, codeDoc); - return codeDoc; - } - - /** - * Helper: build a token request with the given code and verifier. - * @param {string} code The authorization code to exchange. - * @param {string} codeVerifier The code_verifier to include in the request. - * @returns {Request} The constructed request object. - */ - function tokenRequest(code, codeVerifier) { - return createRequest({ - body: { - grant_type: "authorization_code", - code, - redirect_uri: "https://client.example/callback", - code_verifier: codeVerifier, - }, - headers: { - authorization: - "Basic " + - Buffer.from(clientDoc.id + ":" + clientDoc.secret).toString("base64"), - "content-type": "application/x-www-form-urlencoded", - }, - method: "POST", - }); - } - - beforeEach(function () { - db = new DB(); - - // We need authorizationCodes storage on the DB helper - db.authorizationCodes = new Map(); - - db.saveUser(userDoc); - db.saveClient(clientDoc); - - const baseModel = createModel(db); - - oAuth2Server = new OAuth2Server({ - model: { - ...baseModel, - - // --- authorization-code model methods --- - getAuthorizationCode: async function (authorizationCode) { - return db.authorizationCodes.get(authorizationCode) || null; - }, - - saveAuthorizationCode: async function (code, client, user) { - const doc = { ...code, client, user }; - db.authorizationCodes.set(code.authorizationCode, doc); - return doc; - }, - - revokeAuthorizationCode: async function (code) { - return db.authorizationCodes.delete(code.authorizationCode); - }, - - validateScope: async function (user, client, scope) { - return scope; - }, - }, - }); - }); - - // ================================================================== - // Vulnerability 1 – RFC 7636 §4.1 ABNF not enforced on code_verifier - // - // Note: §4.1 ABNF (`43*128unreserved`) is a client-side requirement. - // §4.6 only mandates hash-and-compare on the server. Enforcing the - // ABNF server-side is defense-in-depth to guarantee the ≥256-bit - // entropy minimum described in Appendix B. - // ================================================================== - describe("attack scenario: server accepts RFC7636-invalid code_verifier values", function () { - /** - * RFC 7636 §4.1 (client requirement): - * code-verifier = 43*128unreserved - * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - * - * §4.6 (server verification) only says to hash the verifier and - * compare with the stored code_challenge. It does NOT explicitly - * require the server to reject ABNF-invalid verifiers. - * - * However, accepting short/weak verifiers undermines the security - * model: Appendix B depends on ≥256 bits of entropy (which requires - * at least 43 unreserved characters). Server-side ABNF enforcement - * is therefore essential defense-in-depth. - */ - it("should reject a code_verifier shorter than 43 characters", async () => { - const shortVerifier = "z"; // 1 char – clearly invalid per ABNF - const code = seedAuthorizationCode(shortVerifier); - const request = tokenRequest(code.authorizationCode, shortVerifier); - const response = new Response(); - - // The server should reject this because "z" does not satisfy - // the §4.1 ABNF (43..128 unreserved chars). Although §4.6 does - // not mandate server-side ABNF checks, accepting weak verifiers - // breaks the entropy guarantee of Appendix B. - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `code_verifier`"); - } - - // This assertion documents the a token IS issued - // for an invalid verifier. When the fix is applied this will - // correctly throw, making the test pass again (flip the assertion). - if (tokenIssued) { - throw new Error( - 'Server issued a token for a 1-character code_verifier ("z"). ' + - "RFC 7636 §4.1 ABNF requires 43..128 unreserved characters; accepting shorter " + - "values breaks the entropy guarantee of Appendix B.", - ); - } - }); - - it("should reject a code_verifier of 42 characters (one below minimum)", async () => { - // 42 characters – one below the ABNF minimum of 43 - const shortVerifier = "a".repeat(42); - const code = seedAuthorizationCode(shortVerifier); - const request = tokenRequest(code.authorizationCode, shortVerifier); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `code_verifier`"); - } - - if (tokenIssued) { - throw new Error( - "Server issued a token for a 42-character code_verifier. " + - "RFC 7636 §4.1 ABNF minimum is 43 characters; server-side enforcement " + - "is needed to preserve the entropy guarantee of Appendix B.", - ); - } - }); - - it("should reject a code_verifier of 129 characters (one above maximum)", async () => { - // 129 characters – one above the ABNF maximum of 128 - const longVerifier = "b".repeat(129); - const code = seedAuthorizationCode(longVerifier); - const request = tokenRequest(code.authorizationCode, longVerifier); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `code_verifier`"); - } - - if (tokenIssued) { - throw new Error( - "Server issued a token for a 129-character code_verifier. " + - "RFC 7636 §4.1 ABNF maximum is 128 characters; server-side enforcement " + - "is needed to preserve the entropy guarantee of Appendix B.", - ); - } - }); - - it("should reject a code_verifier with forbidden characters", async () => { - // Contains spaces and special chars that are not in the unreserved set - const badVerifier = "a".repeat(42) + " "; // 43 chars but includes a space - const code = seedAuthorizationCode(badVerifier); - const request = tokenRequest(code.authorizationCode, badVerifier); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `code_verifier`"); - } - - if (tokenIssued) { - throw new Error( - "Server issued a token for a code_verifier containing " + - "forbidden characters (space). RFC 7636 §4.1 restricts to unreserved characters.", - ); - } - }); - }); - - // ================================================================= - // Vulnerability 2 – Authorization code not revoked on failed PKCE, - // enabling brute-force guessing - // - // Note: RFC 7636 is silent on code revocation after failed - // verification. RFC 6749 §4.1.2 says a code used "more than once" - // must be denied, but whether a failed PKCE attempt constitutes - // "use" is ambiguous. Revoking on failure is a security best - // practice to prevent online brute-force of the verifier. - // ================================================================= - describe("attack scenario: authorization code survives failed PKCE verification attempts", function () { - /** - * If an attacker intercepts an authorization code, they can - * repeatedly guess code_verifier values. Because the server only - * revokes the code AFTER successful PKCE verification (in - * handle()), every failed attempt leaves the code intact for the - * next guess. - * - * Neither RFC 7636 nor RFC 6749 explicitly mandate revocation on - * failed verification, but without it the authorization code is - * replayable for unlimited brute-force attempts — a clear security - * weakness. - */ - it("should revoke the authorization code on first failed verifier attempt", async () => { - const realVerifier = "z"; // weak, but accepted by current implementation - const code = seedAuthorizationCode(realVerifier); - - // First attempt with a wrong verifier – should fail - const badRequest = tokenRequest(code.authorizationCode, "a"); - const badResponse = new Response(); - - // before - const codeExists = db.authorizationCodes.has(code.authorizationCode); - codeExists.should.equal( - true, - "Precondition failed: seeded authorization code should exist in DB", - ); - - try { - await oAuth2Server.token(badRequest, badResponse); - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `code_verifier`"); - } - - // After a failed PKCE attempt the authorization code should have - // been revoked (consumed) to prevent further guessing. - const codeStillExists = db.authorizationCodes.has(code.authorizationCode); - - if (codeStillExists) { - throw new Error( - "Authorization code was NOT revoked after a failed " + - "code_verifier attempt. An attacker can keep guessing.", - ); - } - }); - - it("should not allow brute-forcing a weak code_verifier by retrying with the same authorization code", async () => { - // Use a single-char verifier so the search space is tiny - const realVerifier = "z"; - const code = seedAuthorizationCode(realVerifier); - - const alphabet = "abcdefghijklmnopqrstuvwxyz"; - let tokenIssued = false; - let successfulGuess = null; - let tries = 0; - - for (const guess of alphabet) { - tries++; - const request = tokenRequest(code.authorizationCode, guess); - const response = new Response(); - - try { - const token = await oAuth2Server.token(request, response); - if (token && token.accessToken) { - tokenIssued = true; - successfulGuess = guess; - break; - } - } catch (e) { - // wrong guess – continue brute-forcing - } - } - - if (tokenIssued) { - throw new Error( - `Brute-forced code_verifier in ${tries} tries ` + - `(guess="${successfulGuess}"). The authorization code was not ` + - "consumed after failed attempts, allowing online guessing.", - ); - } - }); - - it("should prevent a legitimate 43-char verifier code to be brute-forceable when code is not revoked on failure", async () => { - // Use a valid-length verifier to show the code-reuse issue - // independently of the ABNF length check - const validVerifier = "A".repeat(43); - const code = seedAuthorizationCode(validVerifier); - - // Attempt 1: wrong verifier - const wrongRequest = tokenRequest(code.authorizationCode, "B".repeat(43)); - const wrongResponse = new Response(); - - try { - await oAuth2Server.token(wrongRequest, wrongResponse); - } catch (e) { - // Wrong verifier rejected - e.should.be.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: code verifier is invalid"); - } - - // Attempt 2: correct verifier but should fail because code was revoked - const correctRequest = tokenRequest( - code.authorizationCode, - validVerifier, - ); - const correctResponse = new Response(); - - let tokenIssued = false; - - try { - const token = await oAuth2Server.token(correctRequest, correctResponse); - if (token && token.accessToken) { - tokenIssued = true; - } - } catch (e) { - // This is the correct behaviour after fix: code was revoked and is invalid now - e.should.be.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: authorization code is invalid"); - } - - if (tokenIssued) { - throw new Error( - "Authorization code was still valid after a prior " + - "failed PKCE attempt. The code should have been revoked on the first " + - "failed verification to prevent further guessing.", - ); - } - }); - }); - - // ================================================================= - // Vulnerability 3 - PKCE defaults to "plain" method instead of S256 - // - // Note: RFC 7636 §4.3 specifies that the server assumes "plain" - // when code_challenge_method is absent, and the server MUST support - // "plain". So defaulting to "plain" is technically *RFC-compliant*. - // - // However, "plain" means code_challenge === code_verifier, offering - // zero cryptographic protection. Both the OAuth 2.0 Security BCP - // (draft-ietf-oauth-security-topics §2.1.1) and OAuth 2.1 - // (draft-ietf-oauth-v2-1) deprecate "plain" in favour of S256. - // - // The tests below flag "plain" as a weakness even though it does - // not violate RFC 7636 itself. - // ================================================================= - describe("attack scenario: PKCE defaults to plain method instead of S256 ", function () { - /** - * RFC 7636 §4.2 (client obligation): - * "If the client is capable of using 'S256', it MUST use 'S256'" - * - * RFC 7636 §4.3 (server behaviour): - * The server assumes "plain" when code_challenge_method is absent - * and the server MUST support "plain". This makes defaulting to - * "plain" technically RFC-compliant. - * - * The problem: with "plain", code_challenge === code_verifier. - * Intercepting the authorization request reveals the verifier - * directly — defeating PKCE's purpose for public clients. - * - * Modern guidance (OAuth 2.0 Security BCP §2.1.1, OAuth 2.1) - * deprecates "plain" and recommends servers require S256. - */ - it('should reject or upgrade "plain" PKCE at the token endpoint (beyond-spec hardening)', async () => { - const verifier = "a".repeat(43); // valid ABNF-length verifier - - // With "plain" method, the code_challenge IS the code_verifier. - // We seed an authorization code using "plain" (which is what the - // server would store when code_challenge_method is omitted per - // RFC 7636 §4.3). - const codeValue = - "auth-code-plain-default-" + Math.random().toString(36).slice(2); - const codeDoc = { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), - redirectUri: "https://client.example/callback", - client: clientDoc, - user: userDoc, - scope: ["read"], - codeChallenge: verifier, // plain: challenge === verifier - codeChallengeMethod: "plain", // RFC 7636 §4.3 default - }; - db.authorizationCodes.set(codeValue, codeDoc); - - // Token exchange: provide the verifier in plain text - const request = tokenRequest(codeValue, verifier); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - // would be expected if plain were rejected - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); - } - - // Note: accepting "plain" is RFC 7636-compliant (§4.3 says the - // server MUST support "plain"). However, "plain" means - // code_challenge === code_verifier — zero cryptographic - // protection. OAuth 2.0 Security BCP §2.1.1 and OAuth 2.1 - // deprecate "plain" in favour of S256. - if (tokenIssued) { - throw new Error( - 'Server issued a token using "plain" PKCE method. ' + - 'While RFC 7636 §4.3 requires server support for "plain", the OAuth 2.0 ' + - "Security BCP and OAuth 2.1 deprecate it because code_challenge === code_verifier " + - "offers zero cryptographic protection.", - ); - } - }); - - it("should reject defaults to plain by default (beyond-spec hardening)", async () => { - // Create a separate OAuth2Server instance with enablePlainPKCE enabled. - // When this option is true, the server rejects any PKCE flow that uses - // the "plain" code_challenge_method — even though RFC 7636 §4.3 says - // the server MUST support "plain". This is a hardening measure - // aligned with OAuth 2.0 Security BCP §2.1.1 and OAuth 2.1. which deprecate - // "plain" due to its lack of cryptographic protection. - const baseModel = createModel(db); - const strictServer = new OAuth2Server({ - enablePlainPKCE: false, - model: { - ...baseModel, - getAuthorizationCode: async function (authorizationCode) { - return db.authorizationCodes.get(authorizationCode) || null; - }, - saveAuthorizationCode: async function (code, client, user) { - const doc = { ...code, client, user }; - db.authorizationCodes.set(code.authorizationCode, doc); - return doc; - }, - revokeAuthorizationCode: async function (code) { - return db.authorizationCodes.delete(code.authorizationCode); - }, - validateScope: async function (user, client, scope) { - return scope; - }, - }, - }); - - const verifier = "a".repeat(43); // valid ABNF-length verifier - const codeValue = - "auth-code-reject-plain-" + Math.random().toString(36).slice(2); - db.authorizationCodes.set(codeValue, { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), - redirectUri: "https://client.example/callback", - client: clientDoc, - user: userDoc, - scope: ["read"], - codeChallenge: verifier, // plain: challenge === verifier - codeChallengeMethod: "plain", - }); - - const request = tokenRequest(codeValue, verifier); - const response = new Response(); - - let tokenIssued = false; - let error = null; - - try { - await strictServer.token(request, response); - tokenIssued = true; - } catch (e) { - error = e; - } - - if (tokenIssued) { - throw new Error( - "Server with enablePlainPKCE=false still issued " + - 'a token using "plain" PKCE method. The option should cause the server to ' + - "reject any plain code_challenge_method.", - ); - } - - // When rejected correctly, the server should respond with an error - // indicating that the plain method is not allowed. - (error !== null).should.equal(true); - }); - - it("should not allow an attacker who intercepts the authorize request to use the plain code_challenge as verifier", async () => { - // Scenario: a public client sends an authorize request without - // specifying code_challenge_method. The server defaults to "plain", - // storing code_challenge = code_verifier. - // - // An attacker who intercepts the authorize redirect (which contains - // code_challenge in the query string) now knows the code_verifier. - const verifier = "x".repeat(50); - - // Simulate what the server stores when code_challenge_method is - // omitted (defaults to "plain"): code_challenge = verifier - const stolenChallenge = verifier; // attacker reads this from the authorize request - - const codeValue = - "auth-code-stolen-" + Math.random().toString(36).slice(2); - db.authorizationCodes.set(codeValue, { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), - redirectUri: "https://client.example/callback", - client: clientDoc, - user: userDoc, - scope: ["read"], - codeChallenge: verifier, - codeChallengeMethod: "plain", - }); - - // The attacker uses the stolen code_challenge directly as code_verifier - const request = tokenRequest(codeValue, stolenChallenge); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); - } - - if (tokenIssued) { - throw new Error( - "Attacker redeemed an authorization code by using " + - "the intercepted code_challenge as code_verifier (plain method). " + - "This defeats PKCE entirely for public clients.", - ); - } - }); - - it("should not allow an attacker who intercepts the authorize request to use an invalid code_challenge as verifier", async () => { - // Scenario: a public client sends an authorize request without - // specifying code_challenge_method. The server defaults to "plain", - // storing code_challenge = code_verifier. - // - // An attacker who intercepts the authorize redirect (which contains - // code_challenge in the query string) now knows the code_verifier. - const verifier = "x".repeat(50); - - // Simulate what the server stores when code_challenge_method is - // omitted (defaults to "plain"): code_challenge = verifier - const stolenChallenge = verifier; // attacker reads this from the authorize request - - const codeValue = - "auth-code-stolen-" + Math.random().toString(36).slice(2); - db.authorizationCodes.set(codeValue, { - authorizationCode: codeValue, - expiresAt: new Date(Date.now() + 60000), - redirectUri: "https://client.example/callback", - client: clientDoc, - user: userDoc, - scope: ["read"], - codeChallenge: verifier, - codeChallengeMethod: "forged-xyz", // invalid method stored in DB that could cause a "plain" fallback if not handled properly - }); - - // The attacker uses the stolen code_challenge directly as code_verifier - const request = tokenRequest(codeValue, stolenChallenge); - const response = new Response(); - - let tokenIssued = false; - - try { - await oAuth2Server.token(request, response); - tokenIssued = true; - } catch (e) { - // this is not part of the standard which is why we throw a generic ServerError - e.should.be.instanceOf(ServerError); - e.message.should.equal( - "Server error: no valid hash algorithm available to verify `code_verifier`", - ); - } - - if (tokenIssued) { - throw new Error( - "Attacker redeemed an authorization code by using " + - "the intercepted code_challenge as code_verifier (custom method). " + - "This defeats PKCE entirely for public clients.", - ); - } - }); - }); +describe('PKCE Compliance (RFC 7636)', function () { + // --------------------------------------------------------------- + // Shared fixtures + // --------------------------------------------------------------- + let db, oAuth2Server; + + const userDoc = { + id: 'pkce-user-1', + username: 'pkceuser', + password: 'pkcepass', + }; + const clientDoc = { + id: 'pkce-client', + secret: 'pkce-secret', + grants: ['authorization_code'], + redirectUris: ['https://client.example/callback'], + }; + + /** + * Helper: seed a fresh authorization code into the DB that carries + * a PKCE code challenge (S256). + * @param {string} verifier The code_verifier to use for the code_challenge. Should be a valid string but can be weak (e.g. 1 char) to demonstrate vulnerabilities. + * @param {string} codeValue Optional code value to use (for testing). If not provided, a random one will be generated. + * @param {string} method Optional code_challenge_method to use (default "S256"). For testing the "plain" method, set this to "plain" and ensure the verifier is the same as the challenge. + * @return {object} The authorization code document that was seeded into the DB. + */ + function seedAuthorizationCode(verifier, codeValue, method = 'S256') { + codeValue = codeValue || 'auth-code-' + Math.random().toString(36).slice(2); + const codeChallenge = computeS256Challenge(verifier); + const codeDoc = { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), // 1 min from now + redirectUri: 'https://client.example/callback', + client: clientDoc, + user: userDoc, + scope: ['read'], + codeChallenge, + codeChallengeMethod: method, + }; + // store in DB so getAuthorizationCode can find it + db.authorizationCodes.set(codeValue, codeDoc); + return codeDoc; + } + + /** + * Helper: build a token request with the given code and verifier. + * @param {string} code The authorization code to exchange. + * @param {string} codeVerifier The code_verifier to include in the request. + * @returns {Request} The constructed request object. + */ + function tokenRequest(code, codeVerifier) { + return createRequest({ + body: { + grant_type: 'authorization_code', + code, + redirect_uri: 'https://client.example/callback', + code_verifier: codeVerifier, + }, + headers: { + authorization: + 'Basic ' + + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), + 'content-type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + }); + } + + beforeEach(function () { + db = new DB(); + + // We need authorizationCodes storage on the DB helper + db.authorizationCodes = new Map(); + + db.saveUser(userDoc); + db.saveClient(clientDoc); + + const baseModel = createModel(db); + + oAuth2Server = new OAuth2Server({ + model: { + ...baseModel, + + // --- authorization-code model methods --- + getAuthorizationCode: async function (authorizationCode) { + return db.authorizationCodes.get(authorizationCode) || null; + }, + + saveAuthorizationCode: async function (code, client, user) { + const doc = { ...code, client, user }; + db.authorizationCodes.set(code.authorizationCode, doc); + return doc; + }, + + revokeAuthorizationCode: async function (code) { + return db.authorizationCodes.delete(code.authorizationCode); + }, + + validateScope: async function (user, client, scope) { + return scope; + }, + }, + }); + }); + + // ================================================================== + // Vulnerability 1 – RFC 7636 §4.1 ABNF not enforced on code_verifier + // + // Note: §4.1 ABNF (`43*128unreserved`) is a client-side requirement. + // §4.6 only mandates hash-and-compare on the server. Enforcing the + // ABNF server-side is defense-in-depth to guarantee the ≥256-bit + // entropy minimum described in Appendix B. + // ================================================================== + describe('attack scenario: server accepts RFC7636-invalid code_verifier values', function () { + /** + * RFC 7636 §4.1 (client requirement): + * code-verifier = 43*128unreserved + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * + * §4.6 (server verification) only says to hash the verifier and + * compare with the stored code_challenge. It does NOT explicitly + * require the server to reject ABNF-invalid verifiers. + * + * However, accepting short/weak verifiers undermines the security + * model: Appendix B depends on ≥256 bits of entropy (which requires + * at least 43 unreserved characters). Server-side ABNF enforcement + * is therefore essential defense-in-depth. + */ + it('should reject a code_verifier shorter than 43 characters', async () => { + const shortVerifier = 'z'; // 1 char – clearly invalid per ABNF + const code = seedAuthorizationCode(shortVerifier); + const request = tokenRequest(code.authorizationCode, shortVerifier); + const response = new Response(); + + // The server should reject this because "z" does not satisfy + // the §4.1 ABNF (43..128 unreserved chars). Although §4.6 does + // not mandate server-side ABNF checks, accepting weak verifiers + // breaks the entropy guarantee of Appendix B. + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code_verifier`'); + } + + // This assertion documents the a token IS issued + // for an invalid verifier. When the fix is applied this will + // correctly throw, making the test pass again (flip the assertion). + if (tokenIssued) { + throw new Error( + 'Server issued a token for a 1-character code_verifier ("z"). ' + + 'RFC 7636 §4.1 ABNF requires 43..128 unreserved characters; accepting shorter ' + + 'values breaks the entropy guarantee of Appendix B.', + ); + } + }); + + it('should reject a code_verifier of 42 characters (one below minimum)', async () => { + // 42 characters – one below the ABNF minimum of 43 + const shortVerifier = 'a'.repeat(42); + const code = seedAuthorizationCode(shortVerifier); + const request = tokenRequest(code.authorizationCode, shortVerifier); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code_verifier`'); + } + + if (tokenIssued) { + throw new Error( + 'Server issued a token for a 42-character code_verifier. ' + + 'RFC 7636 §4.1 ABNF minimum is 43 characters; server-side enforcement ' + + 'is needed to preserve the entropy guarantee of Appendix B.', + ); + } + }); + + it('should reject a code_verifier of 129 characters (one above maximum)', async () => { + // 129 characters – one above the ABNF maximum of 128 + const longVerifier = 'b'.repeat(129); + const code = seedAuthorizationCode(longVerifier); + const request = tokenRequest(code.authorizationCode, longVerifier); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code_verifier`'); + } + + if (tokenIssued) { + throw new Error( + 'Server issued a token for a 129-character code_verifier. ' + + 'RFC 7636 §4.1 ABNF maximum is 128 characters; server-side enforcement ' + + 'is needed to preserve the entropy guarantee of Appendix B.', + ); + } + }); + + it('should reject a code_verifier with forbidden characters', async () => { + // Contains spaces and special chars that are not in the unreserved set + const badVerifier = 'a'.repeat(42) + ' '; // 43 chars but includes a space + const code = seedAuthorizationCode(badVerifier); + const request = tokenRequest(code.authorizationCode, badVerifier); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code_verifier`'); + } + + if (tokenIssued) { + throw new Error( + 'Server issued a token for a code_verifier containing ' + + 'forbidden characters (space). RFC 7636 §4.1 restricts to unreserved characters.', + ); + } + }); + }); + + // ================================================================= + // Vulnerability 2 – Authorization code not revoked on failed PKCE, + // enabling brute-force guessing + // + // Note: RFC 7636 is silent on code revocation after failed + // verification. RFC 6749 §4.1.2 says a code used "more than once" + // must be denied, but whether a failed PKCE attempt constitutes + // "use" is ambiguous. Revoking on failure is a security best + // practice to prevent online brute-force of the verifier. + // ================================================================= + describe('attack scenario: authorization code survives failed PKCE verification attempts', function () { + /** + * If an attacker intercepts an authorization code, they can + * repeatedly guess code_verifier values. Because the server only + * revokes the code AFTER successful PKCE verification (in + * handle()), every failed attempt leaves the code intact for the + * next guess. + * + * Neither RFC 7636 nor RFC 6749 explicitly mandate revocation on + * failed verification, but without it the authorization code is + * replayable for unlimited brute-force attempts — a clear security + * weakness. + */ + it('should revoke the authorization code on first failed verifier attempt', async () => { + const realVerifier = 'z'; // weak, but accepted by current implementation + const code = seedAuthorizationCode(realVerifier); + + // First attempt with a wrong verifier – should fail + const badRequest = tokenRequest(code.authorizationCode, 'a'); + const badResponse = new Response(); + + // before + const codeExists = db.authorizationCodes.has(code.authorizationCode); + codeExists.should.equal( + true, + 'Precondition failed: seeded authorization code should exist in DB', + ); + + try { + await oAuth2Server.token(badRequest, badResponse); + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code_verifier`'); + } + + // After a failed PKCE attempt the authorization code should have + // been revoked (consumed) to prevent further guessing. + const codeStillExists = db.authorizationCodes.has(code.authorizationCode); + + if (codeStillExists) { + throw new Error( + 'Authorization code was NOT revoked after a failed ' + + 'code_verifier attempt. An attacker can keep guessing.', + ); + } + }); + + it('should not allow brute-forcing a weak code_verifier by retrying with the same authorization code', async () => { + // Use a single-char verifier so the search space is tiny + const realVerifier = 'z'; + const code = seedAuthorizationCode(realVerifier); + + const alphabet = 'abcdefghijklmnopqrstuvwxyz'; + let tokenIssued = false; + let successfulGuess = null; + let tries = 0; + + for (const guess of alphabet) { + tries++; + const request = tokenRequest(code.authorizationCode, guess); + const response = new Response(); + + try { + const token = await oAuth2Server.token(request, response); + if (token && token.accessToken) { + tokenIssued = true; + successfulGuess = guess; + break; + } + } catch (e) { + // wrong guess – continue brute-forcing + } + } + + if (tokenIssued) { + throw new Error( + `Brute-forced code_verifier in ${tries} tries ` + + `(guess="${successfulGuess}"). The authorization code was not ` + + 'consumed after failed attempts, allowing online guessing.', + ); + } + }); + + it('should prevent a legitimate 43-char verifier code to be brute-forceable when code is not revoked on failure', async () => { + // Use a valid-length verifier to show the code-reuse issue + // independently of the ABNF length check + const validVerifier = 'A'.repeat(43); + const code = seedAuthorizationCode(validVerifier); + + // Attempt 1: wrong verifier + const wrongRequest = tokenRequest(code.authorizationCode, 'B'.repeat(43)); + const wrongResponse = new Response(); + + try { + await oAuth2Server.token(wrongRequest, wrongResponse); + } catch (e) { + // Wrong verifier rejected + e.should.be.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: code verifier is invalid'); + } + + // Attempt 2: correct verifier but should fail because code was revoked + const correctRequest = tokenRequest( + code.authorizationCode, + validVerifier, + ); + const correctResponse = new Response(); + + let tokenIssued = false; + + try { + const token = await oAuth2Server.token(correctRequest, correctResponse); + if (token && token.accessToken) { + tokenIssued = true; + } + } catch (e) { + // This is the correct behaviour after fix: code was revoked and is invalid now + e.should.be.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code is invalid'); + } + + if (tokenIssued) { + throw new Error( + 'Authorization code was still valid after a prior ' + + 'failed PKCE attempt. The code should have been revoked on the first ' + + 'failed verification to prevent further guessing.', + ); + } + }); + }); + + // ================================================================= + // Vulnerability 3 - PKCE defaults to "plain" method instead of S256 + // + // Note: RFC 7636 §4.3 specifies that the server assumes "plain" + // when code_challenge_method is absent, and the server MUST support + // "plain". So defaulting to "plain" is technically *RFC-compliant*. + // + // However, "plain" means code_challenge === code_verifier, offering + // zero cryptographic protection. Both the OAuth 2.0 Security BCP + // (draft-ietf-oauth-security-topics §2.1.1) and OAuth 2.1 + // (draft-ietf-oauth-v2-1) deprecate "plain" in favour of S256. + // + // The tests below flag "plain" as a weakness even though it does + // not violate RFC 7636 itself. + // ================================================================= + describe('attack scenario: PKCE defaults to plain method instead of S256 ', function () { + /** + * RFC 7636 §4.2 (client obligation): + * "If the client is capable of using 'S256', it MUST use 'S256'" + * + * RFC 7636 §4.3 (server behaviour): + * The server assumes "plain" when code_challenge_method is absent + * and the server MUST support "plain". This makes defaulting to + * "plain" technically RFC-compliant. + * + * The problem: with "plain", code_challenge === code_verifier. + * Intercepting the authorization request reveals the verifier + * directly — defeating PKCE's purpose for public clients. + * + * Modern guidance (OAuth 2.0 Security BCP §2.1.1, OAuth 2.1) + * deprecates "plain" and recommends servers require S256. + */ + it('should reject or upgrade "plain" PKCE at the token endpoint (beyond-spec hardening)', async () => { + const verifier = 'a'.repeat(43); // valid ABNF-length verifier + + // With "plain" method, the code_challenge IS the code_verifier. + // We seed an authorization code using "plain" (which is what the + // server would store when code_challenge_method is omitted per + // RFC 7636 §4.3). + const codeValue = + 'auth-code-plain-default-' + Math.random().toString(36).slice(2); + const codeDoc = { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), + redirectUri: 'https://client.example/callback', + client: clientDoc, + user: userDoc, + scope: ['read'], + codeChallenge: verifier, // plain: challenge === verifier + codeChallengeMethod: 'plain', // RFC 7636 §4.3 default + }; + db.authorizationCodes.set(codeValue, codeDoc); + + // Token exchange: provide the verifier in plain text + const request = tokenRequest(codeValue, verifier); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + // would be expected if plain were rejected + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } + + // Note: accepting "plain" is RFC 7636-compliant (§4.3 says the + // server MUST support "plain"). However, "plain" means + // code_challenge === code_verifier — zero cryptographic + // protection. OAuth 2.0 Security BCP §2.1.1 and OAuth 2.1 + // deprecate "plain" in favour of S256. + if (tokenIssued) { + throw new Error( + 'Server issued a token using "plain" PKCE method. ' + + 'While RFC 7636 §4.3 requires server support for "plain", the OAuth 2.0 ' + + 'Security BCP and OAuth 2.1 deprecate it because code_challenge === code_verifier ' + + 'offers zero cryptographic protection.', + ); + } + }); + + it('should reject defaults to plain by default (beyond-spec hardening)', async () => { + // Create a separate OAuth2Server instance with enablePlainPKCE enabled. + // When this option is true, the server rejects any PKCE flow that uses + // the "plain" code_challenge_method — even though RFC 7636 §4.3 says + // the server MUST support "plain". This is a hardening measure + // aligned with OAuth 2.0 Security BCP §2.1.1 and OAuth 2.1. which deprecate + // "plain" due to its lack of cryptographic protection. + const baseModel = createModel(db); + const strictServer = new OAuth2Server({ + enablePlainPKCE: false, + model: { + ...baseModel, + getAuthorizationCode: async function (authorizationCode) { + return db.authorizationCodes.get(authorizationCode) || null; + }, + saveAuthorizationCode: async function (code, client, user) { + const doc = { ...code, client, user }; + db.authorizationCodes.set(code.authorizationCode, doc); + return doc; + }, + revokeAuthorizationCode: async function (code) { + return db.authorizationCodes.delete(code.authorizationCode); + }, + validateScope: async function (user, client, scope) { + return scope; + }, + }, + }); + + const verifier = 'a'.repeat(43); // valid ABNF-length verifier + const codeValue = + 'auth-code-reject-plain-' + Math.random().toString(36).slice(2); + db.authorizationCodes.set(codeValue, { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), + redirectUri: 'https://client.example/callback', + client: clientDoc, + user: userDoc, + scope: ['read'], + codeChallenge: verifier, // plain: challenge === verifier + codeChallengeMethod: 'plain', + }); + + const request = tokenRequest(codeValue, verifier); + const response = new Response(); + + let tokenIssued = false; + let error = null; + + try { + await strictServer.token(request, response); + tokenIssued = true; + } catch (e) { + error = e; + } + + if (tokenIssued) { + throw new Error( + 'Server with enablePlainPKCE=false still issued ' + + 'a token using "plain" PKCE method. The option should cause the server to ' + + 'reject any plain code_challenge_method.', + ); + } + + // When rejected correctly, the server should respond with an error + // indicating that the plain method is not allowed. + (error !== null).should.equal(true); + }); + + it('should not allow an attacker who intercepts the authorize request to use the plain code_challenge as verifier', async () => { + // Scenario: a public client sends an authorize request without + // specifying code_challenge_method. The server defaults to "plain", + // storing code_challenge = code_verifier. + // + // An attacker who intercepts the authorize redirect (which contains + // code_challenge in the query string) now knows the code_verifier. + const verifier = 'x'.repeat(50); + + // Simulate what the server stores when code_challenge_method is + // omitted (defaults to "plain"): code_challenge = verifier + const stolenChallenge = verifier; // attacker reads this from the authorize request + + const codeValue = + 'auth-code-stolen-' + Math.random().toString(36).slice(2); + db.authorizationCodes.set(codeValue, { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), + redirectUri: 'https://client.example/callback', + client: clientDoc, + user: userDoc, + scope: ['read'], + codeChallenge: verifier, + codeChallengeMethod: 'plain', + }); + + // The attacker uses the stolen code_challenge directly as code_verifier + const request = tokenRequest(codeValue, stolenChallenge); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + e.should.be.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } + + if (tokenIssued) { + throw new Error( + 'Attacker redeemed an authorization code by using ' + + 'the intercepted code_challenge as code_verifier (plain method). ' + + 'This defeats PKCE entirely for public clients.', + ); + } + }); + + it('should not allow an attacker who intercepts the authorize request to use an invalid code_challenge as verifier', async () => { + // Scenario: a public client sends an authorize request without + // specifying code_challenge_method. The server defaults to "plain", + // storing code_challenge = code_verifier. + // + // An attacker who intercepts the authorize redirect (which contains + // code_challenge in the query string) now knows the code_verifier. + const verifier = 'x'.repeat(50); + + // Simulate what the server stores when code_challenge_method is + // omitted (defaults to "plain"): code_challenge = verifier + const stolenChallenge = verifier; // attacker reads this from the authorize request + + const codeValue = + 'auth-code-stolen-' + Math.random().toString(36).slice(2); + db.authorizationCodes.set(codeValue, { + authorizationCode: codeValue, + expiresAt: new Date(Date.now() + 60000), + redirectUri: 'https://client.example/callback', + client: clientDoc, + user: userDoc, + scope: ['read'], + codeChallenge: verifier, + codeChallengeMethod: 'forged-xyz', // invalid method stored in DB that could cause a "plain" fallback if not handled properly + }); + + // The attacker uses the stolen code_challenge directly as code_verifier + const request = tokenRequest(codeValue, stolenChallenge); + const response = new Response(); + + let tokenIssued = false; + + try { + await oAuth2Server.token(request, response); + tokenIssued = true; + } catch (e) { + // this is not part of the standard which is why we throw a generic ServerError + e.should.be.instanceOf(ServerError); + e.message.should.equal( + 'Server error: no valid hash algorithm available to verify `code_verifier`', + ); + } + + if (tokenIssued) { + throw new Error( + 'Attacker redeemed an authorization code by using ' + + 'the intercepted code_challenge as code_verifier (custom method). ' + + 'This defeats PKCE entirely for public clients.', + ); + } + }); + }); }); diff --git a/test/compliance/refresh-token-grant-type_test.js b/test/compliance/refresh-token-grant-type_test.js index db72f880..67e2deb2 100644 --- a/test/compliance/refresh-token-grant-type_test.js +++ b/test/compliance/refresh-token-grant-type_test.js @@ -56,186 +56,186 @@ * information about the error, used to provide the client * developer with additional information about the error. */ -const OAuth2Server = require("../.."); -const DB = require("../helpers/db"); -const createModel = require("../helpers/model"); -const createRequest = require("../helpers/request"); -const Response = require("../../lib/response"); -const should = require("chai").should(); +const OAuth2Server = require('../..'); +const DB = require('../helpers/db'); +const createModel = require('../helpers/model'); +const createRequest = require('../helpers/request'); +const Response = require('../../lib/response'); +const should = require('chai').should(); -require("chai").should(); +require('chai').should(); const db = new DB(); const auth = new OAuth2Server({ - model: createModel(db), + model: createModel(db), }); -const user = db.saveUser({ id: 1, username: "test", password: "test" }); +const user = db.saveUser({ id: 1, username: 'test', password: 'test' }); const client = db.saveClient({ - id: "a", - secret: "b", - grants: ["password", "refresh_token"], + id: 'a', + secret: 'b', + grants: ['password', 'refresh_token'], }); -const scope = "read write"; +const scope = 'read write'; function createLoginRequest() { - return createRequest({ - body: { - grant_type: "password", - username: user.username, - password: user.password, - scope, - }, - headers: { - authorization: - "Basic " + - Buffer.from(client.id + ":" + client.secret).toString("base64"), - "content-type": "application/x-www-form-urlencoded", - }, - method: "POST", - }); + return createRequest({ + body: { + grant_type: 'password', + username: user.username, + password: user.password, + scope, + }, + headers: { + authorization: + 'Basic ' + + Buffer.from(client.id + ':' + client.secret).toString('base64'), + 'content-type': 'application/x-www-form-urlencoded', + }, + method: 'POST', + }); } function createRefreshRequest(refresh_token) { - return createRequest({ - method: "POST", - body: { - grant_type: "refresh_token", - refresh_token, - scope, - }, - headers: { - authorization: - "Basic " + - Buffer.from(client.id + ":" + client.secret).toString("base64"), - "content-type": "application/x-www-form-urlencoded", - }, - }); + return createRequest({ + method: 'POST', + body: { + grant_type: 'refresh_token', + refresh_token, + scope, + }, + headers: { + authorization: + 'Basic ' + + Buffer.from(client.id + ':' + client.secret).toString('base64'), + 'content-type': 'application/x-www-form-urlencoded', + }, + }); } -describe("RefreshTokenGrantType Compliance", function () { - describe("With scope", function () { - it("Should generate token response", async function () { - const request = createLoginRequest(); - const response = new Response({}); +describe('RefreshTokenGrantType Compliance', function () { + describe('With scope', function () { + it('Should generate token response', async function () { + const request = createLoginRequest(); + const response = new Response({}); - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - const token = await auth.token(refreshRequest, refreshResponse, {}); + const token = await auth.token(refreshRequest, refreshResponse, {}); - refreshResponse.body.token_type.should.equal("Bearer"); - refreshResponse.body.access_token.should.equal(token.accessToken); - refreshResponse.body.refresh_token.should.equal(token.refreshToken); - refreshResponse.body.expires_in.should.be.a("number"); - refreshResponse.body.scope.should.eql("read write"); + refreshResponse.body.token_type.should.equal('Bearer'); + refreshResponse.body.access_token.should.equal(token.accessToken); + refreshResponse.body.refresh_token.should.equal(token.refreshToken); + refreshResponse.body.expires_in.should.be.a('number'); + refreshResponse.body.scope.should.eql('read write'); - token.accessToken.should.be.a("string"); - token.refreshToken.should.be.a("string"); - token.accessTokenExpiresAt.should.be.a("date"); - token.refreshTokenExpiresAt.should.be.a("date"); - token.scope.should.eql(["read", "write"]); + token.accessToken.should.be.a('string'); + token.refreshToken.should.be.a('string'); + token.accessTokenExpiresAt.should.be.a('date'); + token.refreshTokenExpiresAt.should.be.a('date'); + token.scope.should.eql(['read', 'write']); - db.accessTokens.has(token.accessToken).should.equal(true); - db.refreshTokens.has(token.refreshToken).should.equal(true); - }); + db.accessTokens.has(token.accessToken).should.equal(true); + db.refreshTokens.has(token.refreshToken).should.equal(true); + }); - it("Should throw invalid_grant error", async function () { - const request = createRefreshRequest("invalid"); - const response = new Response({}); + it('Should throw invalid_grant error', async function () { + const request = createRefreshRequest('invalid'); + const response = new Response({}); - await auth - .token(request, response, {}) - .then(() => { - throw Error("Should not reach this"); - }) - .catch((err) => { - err.name.should.equal("invalid_grant"); - }); - }); + await auth + .token(request, response, {}) + .then(() => { + throw Error('Should not reach this'); + }) + .catch((err) => { + err.name.should.equal('invalid_grant'); + }); + }); - it("Should throw invalid_scope error", async function () { - const request = createLoginRequest(); - const response = new Response({}); + it('Should throw invalid_scope error', async function () { + const request = createLoginRequest(); + const response = new Response({}); - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - refreshRequest.body.scope = "invalid"; + refreshRequest.body.scope = 'invalid'; - await auth - .token(refreshRequest, refreshResponse, {}) - .then(should.fail) - .catch((err) => { - err.name.should.equal("invalid_scope"); - }); - }); + await auth + .token(refreshRequest, refreshResponse, {}) + .then(should.fail) + .catch((err) => { + err.name.should.equal('invalid_scope'); + }); + }); - it("Should throw error if requested scope is greater than original scope", async function () { - const request = createLoginRequest(); - const response = new Response({}); + it('Should throw error if requested scope is greater than original scope', async function () { + const request = createLoginRequest(); + const response = new Response({}); - request.body.scope = "read"; + request.body.scope = 'read'; - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - refreshRequest.scope = "read write"; + refreshRequest.scope = 'read write'; - await auth - .token(refreshRequest, refreshResponse, {}) - .then(should.fail) - .catch((err) => { - err.name.should.equal("invalid_scope"); - }); - }); + await auth + .token(refreshRequest, refreshResponse, {}) + .then(should.fail) + .catch((err) => { + err.name.should.equal('invalid_scope'); + }); + }); - it("Should throw error if a scope is requested without a previous scope", async function () { - const request = createLoginRequest(); - const response = new Response({}); + it('Should throw error if a scope is requested without a previous scope', async function () { + const request = createLoginRequest(); + const response = new Response({}); - delete request.body.scope; + delete request.body.scope; - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - refreshRequest.scope = "read write"; + refreshRequest.scope = 'read write'; - await auth - .token(refreshRequest, refreshResponse, {}) - .then(should.fail) - .catch((err) => { - err.name.should.equal("invalid_scope"); - }); - }); + await auth + .token(refreshRequest, refreshResponse, {}) + .then(should.fail) + .catch((err) => { + err.name.should.equal('invalid_scope'); + }); + }); - it("Should create refresh token with smaller scope", async function () { - const request = createLoginRequest(); - const response = new Response({}); + it('Should create refresh token with smaller scope', async function () { + const request = createLoginRequest(); + const response = new Response({}); - const credentials = await auth.token(request, response, {}); + const credentials = await auth.token(request, response, {}); - const refreshRequest = createRefreshRequest(credentials.refreshToken); - const refreshResponse = new Response({}); + const refreshRequest = createRefreshRequest(credentials.refreshToken); + const refreshResponse = new Response({}); - refreshRequest.body.scope = "read"; + refreshRequest.body.scope = 'read'; - const token = await auth.token(refreshRequest, refreshResponse, {}); + const token = await auth.token(refreshRequest, refreshResponse, {}); - refreshResponse.body.token_type.should.equal("Bearer"); - refreshResponse.body.access_token.should.equal(token.accessToken); - refreshResponse.body.refresh_token.should.equal(token.refreshToken); - refreshResponse.body.expires_in.should.be.a("number"); - refreshResponse.body.scope.should.eql("read"); - }); - }); + refreshResponse.body.token_type.should.equal('Bearer'); + refreshResponse.body.access_token.should.equal(token.accessToken); + refreshResponse.body.refresh_token.should.equal(token.refreshToken); + refreshResponse.body.expires_in.should.be.a('number'); + refreshResponse.body.scope.should.eql('read'); + }); + }); }); diff --git a/test/helpers/db.js b/test/helpers/db.js index 7d5444fb..fbd9a36b 100644 --- a/test/helpers/db.js +++ b/test/helpers/db.js @@ -1,70 +1,70 @@ class DB { - constructor() { - this.users = new Map(); - this.clients = []; - this.accessTokens = new Map(); - this.refreshTokens = new Map(); - } + constructor() { + this.users = new Map(); + this.clients = []; + this.accessTokens = new Map(); + this.refreshTokens = new Map(); + } - saveUser(user) { - this.users.set(user.id, user); + saveUser(user) { + this.users.set(user.id, user); - return user; - } + return user; + } - findUser(username, password) { - return Array.from(this.users.values()).find((user) => { - return user.username === username && user.password === password; - }); - } + findUser(username, password) { + return Array.from(this.users.values()).find((user) => { + return user.username === username && user.password === password; + }); + } - findUserById(id) { - return this.users.get(id); - } + findUserById(id) { + return this.users.get(id); + } - saveClient(client) { - this.clients.push(client); + saveClient(client) { + this.clients.push(client); - return client; - } + return client; + } - findClient(clientId, clientSecret) { - return this.clients.find((client) => { - if (clientSecret) { - return client.id === clientId && client.secret === clientSecret; - } else { - return client.id === clientId; - } - }); - } + findClient(clientId, clientSecret) { + return this.clients.find((client) => { + if (clientSecret) { + return client.id === clientId && client.secret === clientSecret; + } else { + return client.id === clientId; + } + }); + } - findClientById(id) { - return this.clients.find((client) => client.id === id); - } + findClientById(id) { + return this.clients.find((client) => client.id === id); + } - saveAccessToken(accessToken, meta) { - this.accessTokens.set(accessToken, meta); - } + saveAccessToken(accessToken, meta) { + this.accessTokens.set(accessToken, meta); + } - findAccessToken(accessToken) { - return this.accessTokens.get(accessToken); - } + findAccessToken(accessToken) { + return this.accessTokens.get(accessToken); + } - deleteAccessToken(accessToken) { - this.accessTokens.delete(accessToken); - } + deleteAccessToken(accessToken) { + this.accessTokens.delete(accessToken); + } - saveRefreshToken(refreshToken, meta) { - this.refreshTokens.set(refreshToken, meta); - } + saveRefreshToken(refreshToken, meta) { + this.refreshTokens.set(refreshToken, meta); + } - findRefreshToken(refreshToken) { - return this.refreshTokens.get(refreshToken); - } + findRefreshToken(refreshToken) { + return this.refreshTokens.get(refreshToken); + } - deleteRefreshToken(refreshToken) { - this.refreshTokens.delete(refreshToken); - } + deleteRefreshToken(refreshToken) { + this.refreshTokens.delete(refreshToken); + } } module.exports = DB; diff --git a/test/helpers/model.js b/test/helpers/model.js index 7faccdc7..ccab6461 100644 --- a/test/helpers/model.js +++ b/test/helpers/model.js @@ -1,99 +1,99 @@ -const Model = require("../../lib/model"); -const scopes = ["read", "write"]; +const Model = require('../../lib/model'); +const scopes = ['read', 'write']; function createModel(db) { - async function getUser(username, password) { - return db.findUser(username, password); - } - - async function getClient(clientId, clientSecret) { - return db.findClient(clientId, clientSecret); - } - - async function saveToken(token, client, user) { - if (token.scope && !Array.isArray(token.scope)) { - throw new Error("Scope should internally be an array"); - } - const meta = { - clientId: client.id, - userId: user.id, - scope: token.scope, - accessTokenExpiresAt: token.accessTokenExpiresAt, - refreshTokenExpiresAt: token.refreshTokenExpiresAt, - }; - - token.client = client; - token.user = user; - - if (token.accessToken) { - db.saveAccessToken(token.accessToken, meta); - } - - if (token.refreshToken) { - db.saveRefreshToken(token.refreshToken, meta); - } - - return token; - } - - async function getAccessToken(accessToken) { - const meta = db.findAccessToken(accessToken); - - if (!meta) { - return false; - } - if (meta.scope && !Array.isArray(meta.scope)) { - throw new Error("Scope should internally be an array"); - } - return { - accessToken, - accessTokenExpiresAt: meta.accessTokenExpiresAt, - user: db.findUserById(meta.userId), - client: db.findClientById(meta.clientId), - scope: meta.scope, - }; - } - - async function getRefreshToken(refreshToken) { - const meta = db.findRefreshToken(refreshToken); - - if (!meta) { - return false; - } - if (meta.scope && !Array.isArray(meta.scope)) { - throw new Error("Scope should internally be an array"); - } - return { - refreshToken, - refreshTokenExpiresAt: meta.refreshTokenExpiresAt, - user: db.findUserById(meta.userId), - client: db.findClientById(meta.clientId), - scope: meta.scope, - }; - } - - async function revokeToken(token) { - db.deleteRefreshToken(token.refreshToken); - - return true; - } - - async function verifyScope(token, scope) { - if (!Array.isArray(scope)) { - throw new Error("Scope should internally be an array"); - } - return scope.every((s) => scopes.includes(s)); - } - - return Model.from({ - getUser, - getClient, - saveToken, - getAccessToken, - getRefreshToken, - revokeToken, - verifyScope, - }); + async function getUser(username, password) { + return db.findUser(username, password); + } + + async function getClient(clientId, clientSecret) { + return db.findClient(clientId, clientSecret); + } + + async function saveToken(token, client, user) { + if (token.scope && !Array.isArray(token.scope)) { + throw new Error('Scope should internally be an array'); + } + const meta = { + clientId: client.id, + userId: user.id, + scope: token.scope, + accessTokenExpiresAt: token.accessTokenExpiresAt, + refreshTokenExpiresAt: token.refreshTokenExpiresAt, + }; + + token.client = client; + token.user = user; + + if (token.accessToken) { + db.saveAccessToken(token.accessToken, meta); + } + + if (token.refreshToken) { + db.saveRefreshToken(token.refreshToken, meta); + } + + return token; + } + + async function getAccessToken(accessToken) { + const meta = db.findAccessToken(accessToken); + + if (!meta) { + return false; + } + if (meta.scope && !Array.isArray(meta.scope)) { + throw new Error('Scope should internally be an array'); + } + return { + accessToken, + accessTokenExpiresAt: meta.accessTokenExpiresAt, + user: db.findUserById(meta.userId), + client: db.findClientById(meta.clientId), + scope: meta.scope, + }; + } + + async function getRefreshToken(refreshToken) { + const meta = db.findRefreshToken(refreshToken); + + if (!meta) { + return false; + } + if (meta.scope && !Array.isArray(meta.scope)) { + throw new Error('Scope should internally be an array'); + } + return { + refreshToken, + refreshTokenExpiresAt: meta.refreshTokenExpiresAt, + user: db.findUserById(meta.userId), + client: db.findClientById(meta.clientId), + scope: meta.scope, + }; + } + + async function revokeToken(token) { + db.deleteRefreshToken(token.refreshToken); + + return true; + } + + async function verifyScope(token, scope) { + if (!Array.isArray(scope)) { + throw new Error('Scope should internally be an array'); + } + return scope.every((s) => scopes.includes(s)); + } + + return Model.from({ + getUser, + getClient, + saveToken, + getAccessToken, + getRefreshToken, + revokeToken, + verifyScope, + }); } module.exports = createModel; diff --git a/test/helpers/request.js b/test/helpers/request.js index 98e1c255..91b278bc 100644 --- a/test/helpers/request.js +++ b/test/helpers/request.js @@ -1,17 +1,17 @@ -const Request = require("../../lib/request"); +const Request = require('../../lib/request'); module.exports = (request) => { - const req = new Request({ - query: {}, - body: {}, - headers: {}, - method: "GET", - ...request, - }); + const req = new Request({ + query: {}, + body: {}, + headers: {}, + method: 'GET', + ...request, + }); - req.is = function (header) { - return this.headers["content-type"] === header; - }; + req.is = function (header) { + return this.headers['content-type'] === header; + }; - return req; + return req; }; diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js index ec300859..c452b5b3 100644 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -1,308 +1,308 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AbstractGrantType = require("../../../lib/grant-types/abstract-grant-type"); -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const Model = require("../../../lib/model"); -const Request = require("../../../lib/request"); -const InvalidScopeError = require("../../../lib/errors/invalid-scope-error"); -const should = require("chai").should(); +const AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const Model = require('../../../lib/model'); +const Request = require('../../../lib/request'); +const InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); +const should = require('chai').should(); /** * Test `AbstractGrantType` integration. */ -describe("AbstractGrantType integration", function () { - describe("constructor()", function () { - it("should throw an error if `options.accessTokenLifetime` is missing", function () { - try { - new AbstractGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `accessTokenLifetime`"); - } - }); - - it("should throw an error if `options.model` is missing", function () { - try { - new AbstractGrantType({ accessTokenLifetime: 123 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `model`"); - } - }); - - it("should set the `accessTokenLifetime`", function () { - const grantType = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - }); - - grantType.accessTokenLifetime.should.equal(123); - }); - - it("should set the `model`", function () { - const model = Model.from({ async generateAccessToken() {} }); - const grantType = new AbstractGrantType({ - accessTokenLifetime: 123, - model: model, - }); - - grantType.model.should.equal(model); - }); - - it("should set the `refreshTokenLifetime`", function () { - const grantType = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - refreshTokenLifetime: 456, - }); - - grantType.refreshTokenLifetime.should.equal(456); - }); - }); - - describe("generateAccessToken()", function () { - it("should return an access token", async function () { - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - refreshTokenLifetime: 456, - }); - const accessToken = await handler.generateAccessToken(); - accessToken.should.be.a.sha256(); - }); - - it("should support promises", async function () { - const model = Model.from({ - generateAccessToken: async function () { - return "long-hash-foo-bar"; - }, - }); - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: model, - refreshTokenLifetime: 456, - }); - const accessToken = await handler.generateAccessToken(); - accessToken.should.equal("long-hash-foo-bar"); - }); - - it("should support non-promises", async function () { - const model = Model.from({ - generateAccessToken: function () { - return "long-hash-foo-bar"; - }, - }); - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: model, - refreshTokenLifetime: 456, - }); - const accessToken = await handler.generateAccessToken(); - accessToken.should.equal("long-hash-foo-bar"); - }); - }); - - describe("generateRefreshToken()", function () { - it("should return a refresh token", async function () { - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - refreshTokenLifetime: 456, - }); - const refreshToken = await handler.generateRefreshToken(); - refreshToken.should.be.a.sha256(); - }); - - it("should support promises", async function () { - const model = Model.from({ - generateRefreshToken: async function () { - return "long-hash-foo-bar"; - }, - }); - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: model, - refreshTokenLifetime: 456, - }); - const refreshToken = await handler.generateRefreshToken(); - refreshToken.should.equal("long-hash-foo-bar"); - }); - - it("should support non-promises", async function () { - const model = Model.from({ - generateRefreshToken: function () { - return "long-hash-foo-bar"; - }, - }); - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: model, - refreshTokenLifetime: 456, - }); - const refreshToken = await handler.generateRefreshToken(); - refreshToken.should.equal("long-hash-foo-bar"); - }); - }); - - describe("getAccessTokenExpiresAt()", function () { - it("should return a date", function () { - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - refreshTokenLifetime: 456, - }); - - handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); - }); - }); - - describe("getRefreshTokenExpiresAt()", function () { - it("should return a refresh token", function () { - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - refreshTokenLifetime: 456, - }); - - handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); - }); - }); - - describe("getScope()", function () { - it("should throw an error if `scope` is invalid", function () { - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - refreshTokenLifetime: 456, - }); - const request = new Request({ - body: { scope: "øå€£‰" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - handler.getScope(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal("Invalid parameter: `scope`"); - } - }); - - it("should allow the `scope` to be `undefined`", function () { - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - refreshTokenLifetime: 456, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - should.not.exist(handler.getScope(request)); - }); - - it("should return the scope", function () { - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - refreshTokenLifetime: 456, - }); - const request = new Request({ - body: { scope: "foo" }, - headers: {}, - method: {}, - query: {}, - }); - - handler.getScope(request).should.eql(["foo"]); - }); - }); - - describe("validateScope()", function () { - it("accepts the scope, if the model does not implement it", async function () { - const scope = ["some,scope,this,that"]; - const user = { id: 123 }; - const client = { id: 456 }; - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model: {}, - refreshTokenLifetime: 456, - }); - const validated = await handler.validateScope(user, client, scope); - validated.should.eql(scope); - }); - - it("accepts the scope, if the model accepts it", async function () { - const scope = ["some,scope,this,that"]; - const user = { id: 123 }; - const client = { id: 456 }; - - const model = Model.from({ - async validateScope(_user, _client, _scope) { - // make sure the model received the correct args - _user.should.deep.equal(user); - _client.should.deep.equal(_client); - _scope.should.eql(scope); - - return scope; - }, - }); - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model, - refreshTokenLifetime: 456, - }); - const validated = await handler.validateScope(user, client, scope); - validated.should.eql(scope); - }); - - it("throws if the model rejects the scope", async function () { - const scope = ["some,scope,this,that"]; - const user = { id: 123 }; - const client = { id: 456 }; - const returnTypes = [undefined, null, false, 0, ""]; - - for (const type of returnTypes) { - const model = Model.from({ - async validateScope(_user, _client, _scope) { - // make sure the model received the correct args - _user.should.deep.equal(user); - _client.should.deep.equal(_client); - _scope.should.eql(scope); - - return type; - }, - }); - const handler = new AbstractGrantType({ - accessTokenLifetime: 123, - model, - refreshTokenLifetime: 456, - }); - - try { - await handler.validateScope(user, client, scope); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal("Invalid scope: Requested scope is invalid"); - } - } - }); - }); +describe('AbstractGrantType integration', function () { + describe('constructor()', function () { + it('should throw an error if `options.accessTokenLifetime` is missing', function () { + try { + new AbstractGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + it('should throw an error if `options.model` is missing', function () { + try { + new AbstractGrantType({ accessTokenLifetime: 123 }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should set the `accessTokenLifetime`', function () { + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + }); + + grantType.accessTokenLifetime.should.equal(123); + }); + + it('should set the `model`', function () { + const model = Model.from({ async generateAccessToken() {} }); + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.model.should.equal(model); + }); + + it('should set the `refreshTokenLifetime`', function () { + const grantType = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + grantType.refreshTokenLifetime.should.equal(456); + }); + }); + + describe('generateAccessToken()', function () { + it('should return an access token', async function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const accessToken = await handler.generateAccessToken(); + accessToken.should.be.a.sha256(); + }); + + it('should support promises', async function () { + const model = Model.from({ + generateAccessToken: async function () { + return 'long-hash-foo-bar'; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 456, + }); + const accessToken = await handler.generateAccessToken(); + accessToken.should.equal('long-hash-foo-bar'); + }); + + it('should support non-promises', async function () { + const model = Model.from({ + generateAccessToken: function () { + return 'long-hash-foo-bar'; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 456, + }); + const accessToken = await handler.generateAccessToken(); + accessToken.should.equal('long-hash-foo-bar'); + }); + }); + + describe('generateRefreshToken()', function () { + it('should return a refresh token', async function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.be.a.sha256(); + }); + + it('should support promises', async function () { + const model = Model.from({ + generateRefreshToken: async function () { + return 'long-hash-foo-bar'; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 456, + }); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.equal('long-hash-foo-bar'); + }); + + it('should support non-promises', async function () { + const model = Model.from({ + generateRefreshToken: function () { + return 'long-hash-foo-bar'; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 456, + }); + const refreshToken = await handler.generateRefreshToken(); + refreshToken.should.equal('long-hash-foo-bar'); + }); + }); + + describe('getAccessTokenExpiresAt()', function () { + it('should return a date', function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); + }); + }); + + describe('getRefreshTokenExpiresAt()', function () { + it('should return a refresh token', function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + + handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); + }); + }); + + describe('getScope()', function () { + it('should throw an error if `scope` is invalid', function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: { scope: 'øå€£‰' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + handler.getScope(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid parameter: `scope`'); + } + }); + + it('should allow the `scope` to be `undefined`', function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + should.not.exist(handler.getScope(request)); + }); + + it('should return the scope', function () { + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const request = new Request({ + body: { scope: 'foo' }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getScope(request).should.eql(['foo']); + }); + }); + + describe('validateScope()', function () { + it('accepts the scope, if the model does not implement it', async function () { + const scope = ['some,scope,this,that']; + const user = { id: 123 }; + const client = { id: 456 }; + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model: {}, + refreshTokenLifetime: 456, + }); + const validated = await handler.validateScope(user, client, scope); + validated.should.eql(scope); + }); + + it('accepts the scope, if the model accepts it', async function () { + const scope = ['some,scope,this,that']; + const user = { id: 123 }; + const client = { id: 456 }; + + const model = Model.from({ + async validateScope(_user, _client, _scope) { + // make sure the model received the correct args + _user.should.deep.equal(user); + _client.should.deep.equal(_client); + _scope.should.eql(scope); + + return scope; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + const validated = await handler.validateScope(user, client, scope); + validated.should.eql(scope); + }); + + it('throws if the model rejects the scope', async function () { + const scope = ['some,scope,this,that']; + const user = { id: 123 }; + const client = { id: 456 }; + const returnTypes = [undefined, null, false, 0, '']; + + for (const type of returnTypes) { + const model = Model.from({ + async validateScope(_user, _client, _scope) { + // make sure the model received the correct args + _user.should.deep.equal(user); + _client.should.deep.equal(_client); + _scope.should.eql(scope); + + return type; + }, + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 123, + model, + refreshTokenLifetime: 456, + }); + + try { + await handler.validateScope(user, client, scope); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid scope: Requested scope is invalid'); + } + } + }); + }); }); diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index 0495e77f..f1b83a99 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -1,955 +1,955 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AuthorizationCodeGrantType = require("../../../lib/grant-types/authorization-code-grant-type"); -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); -const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); -const Model = require("../../../lib/model"); -const Request = require("../../../lib/request"); -const ServerError = require("../../../lib/errors/server-error"); -const should = require("chai").should(); +const AuthorizationCodeGrantType = require('../../../lib/grant-types/authorization-code-grant-type'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); +const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +const Model = require('../../../lib/model'); +const Request = require('../../../lib/request'); +const ServerError = require('../../../lib/errors/server-error'); +const should = require('chai').should(); /** * Test `AuthorizationCodeGrantType` integration. */ -describe("AuthorizationCodeGrantType integration", function () { - describe("constructor()", function () { - it("should throw an error if `model` is missing", function () { - try { - new AuthorizationCodeGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `model`"); - } - }); - - it("should throw an error if the model does not implement `getAuthorizationCode()`", function () { - try { - new AuthorizationCodeGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `getAuthorizationCode()`", - ); - } - }); - - it("should throw an error if the model does not implement `revokeAuthorizationCode()`", function () { - try { - const model = Model.from({ - getAuthorizationCode: function () {}, - }); - - new AuthorizationCodeGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `revokeAuthorizationCode()`", - ); - } - }); - - it("should throw an error if the model does not implement `saveToken()`", function () { - try { - const model = Model.from({ - getAuthorizationCode: function () {}, - revokeAuthorizationCode: function () {}, - }); - - new AuthorizationCodeGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `saveToken()`", - ); - } - }); - }); - - describe("handle()", function () { - it("should throw an error if `request` is missing", async function () { - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - - try { - await grantType.handle(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `request`"); - } - }); - - it("should throw an error if `client` is invalid (not in code)", async function () { - const client = { id: 1234 }; - const model = Model.from({ - getAuthorizationCode: function (code) { - code.should.equal(123456789); - return { - authorizationCode: 12345, - expiresAt: new Date(new Date() * 2), - user: {}, - }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 123456789 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.handle(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - "Server error: `getAuthorizationCode()` did not return a `client` object", - ); - } - }); - - it("should throw an error if `client` is missing", function () { - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - grantType.handle(request, null); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `client`"); - } - }); - - it("should return a token", async function () { - const client = { id: "foobar" }; - const scope = ["fooscope"]; - const user = { name: "foouser" }; - const codeDoc = { - authorizationCode: 12345, - expiresAt: new Date(new Date() * 2), - client, - user, - scope, - }; - const model = Model.from({ - getAuthorizationCode: async function (code) { - code.should.equal("code-1234"); - - return codeDoc; - }, - revokeAuthorizationCode: async function (_codeDoc) { - _codeDoc.should.deep.equal(codeDoc); - return true; - }, - validateScope: async function (_user, _client, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return scope; - }, - generateAccessToken: async function (_client, _user, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return "long-access-token-hash"; - }, - generateRefreshToken: async function (_client, _user, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return "long-refresh-token-hash"; - }, - saveToken: async function (_token, _client, _user) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _token.accessToken.should.equal("long-access-token-hash"); - _token.refreshToken.should.equal("long-refresh-token-hash"); - _token.authorizationCode.should.equal(codeDoc.authorizationCode); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return _token; - }, - }); - - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: "code-1234" }, - headers: {}, - method: {}, - query: {}, - }); - - const token = await grantType.handle(request, client); - token.accessToken.should.equal("long-access-token-hash"); - token.refreshToken.should.equal("long-refresh-token-hash"); - token.authorizationCode.should.equal(codeDoc.authorizationCode); - token.accessTokenExpiresAt.should.be.instanceOf(Date); - token.refreshTokenExpiresAt.should.be.instanceOf(Date); - }); - - it("should support promises", function () { - const client = { id: "foobar" }; - const model = Model.from({ - getAuthorizationCode: function () { - return { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date() * 2), - user: {}, - }; - }, - revokeAuthorizationCode: function () { - return true; - }, - saveToken: function () {}, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const client = { id: "foobar" }; - const model = Model.from({ - getAuthorizationCode: function () { - return { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date() * 2), - user: {}, - }; - }, - revokeAuthorizationCode: function () { - return true; - }, - saveToken: function () {}, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe("getAuthorizationCode()", function () { - it("should throw an error if the request body does not contain `code`", async function () { - const client = {}; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getAuthorizationCode(request, client); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Missing parameter: `code`"); - } - }); - - it("should throw an error if `code` is invalid", async function () { - const client = {}; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: "øå€£‰" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `code`"); - } - }); - - it("should throw an error if `authorizationCode` is missing", async function () { - const client = {}; - const model = Model.from({ - getAuthorizationCode: async function () {}, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: authorization code is invalid"); - } - }); - - it("should throw an error if `authorizationCode.client` is missing", async function () { - const client = {}; - const model = Model.from({ - getAuthorizationCode: async function () { - return { authorizationCode: 12345 }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - "Server error: `getAuthorizationCode()` did not return a `client` object", - ); - } - }); - - it("should throw an error if `authorizationCode.expiresAt` is missing", async function () { - const client = {}; - const model = Model.from({ - getAuthorizationCode: async function () { - return { authorizationCode: 12345, client: {}, user: {} }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - "Server error: `expiresAt` must be a Date instance", - ); - } - }); - - it("should throw an error if `authorizationCode.user` is missing", async function () { - const client = {}; - const model = Model.from({ - getAuthorizationCode: async function () { - return { - authorizationCode: 12345, - client: {}, - expiresAt: new Date(), - }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - "Server error: `getAuthorizationCode()` did not return a `user` object", - ); - } - }); - - it("should throw an error if the client id does not match", async function () { - const client = { id: 123 }; - const model = Model.from({ - getAuthorizationCode: async function () { - return { - authorizationCode: 12345, - expiresAt: new Date(), - client: { id: 456 }, - user: {}, - }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: authorization code is invalid"); - } - }); - - it("should throw an error if the auth code is expired", async function () { - const client = { id: 123 }; - const date = new Date(new Date() / 2); - const model = Model.from({ - getAuthorizationCode: async function () { - return { - authorizationCode: 12345, - client: { id: 123 }, - expiresAt: date, - user: {}, - }; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: authorization code has expired"); - } - }); - - it("should throw an error if the `redirectUri` is invalid (format)", async function () { - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date() * 2), - redirectUri: "foobar", - user: {}, - }; - const client = { id: "foobar" }; - const model = Model.from({ - getAuthorizationCode: async function () { - return authorizationCode; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getAuthorizationCode(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - "Invalid grant: `redirect_uri` is not a valid URI", - ); - } - }); - - it("should return an auth code", async function () { - const authorizationCode = { - authorizationCode: 1234567, - client: { id: "foobar" }, - expiresAt: new Date(new Date() * 2), - user: {}, - }; - const client = { id: "foobar" }; - const model = Model.from({ - getAuthorizationCode: async function (_code) { - _code.should.equal(12345); - return authorizationCode; - }, - revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - const code = await grantType.getAuthorizationCode(request, client); - code.should.deep.equal(authorizationCode); - }); - - it("should support promises", function () { - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date() * 2), - user: {}, - }; - const client = { id: "foobar" }; - const model = Model.from({ - getAuthorizationCode: async function () { - return authorizationCode; - }, - revokeAuthorizationCode: function () {}, - saveToken: function () {}, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - grantType - .getAuthorizationCode(request, client) - .should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date() * 2), - user: {}, - }; - const client = { id: "foobar" }; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - revokeAuthorizationCode: function () {}, - saveToken: function () {}, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - grantType - .getAuthorizationCode(request, client) - .should.be.an.instanceOf(Promise); - }); - }); - - describe("validateRedirectUri()", function () { - it("should throw an error if `redirectUri` is missing", function () { - const authorizationCode = { - authorizationCode: 12345, - client: {}, - expiresAt: new Date(new Date() / 2), - redirectUri: "http://foo.bar", - user: {}, - }; - const model = Model.from({ - getAuthorizationCode: function () {}, - revokeAuthorizationCode: function () { - return authorizationCode; - }, - saveToken: function () {}, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - grantType.validateRedirectUri(request, authorizationCode); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: `redirect_uri` is not a valid URI", - ); - } - }); - - it("should throw an error if `redirectUri` is invalid", function () { - const authorizationCode = { - authorizationCode: 12345, - client: {}, - expiresAt: new Date(new Date() / 2), - redirectUri: "http://foo.bar", - user: {}, - }; - const model = Model.from({ - getAuthorizationCode: function () {}, - revokeAuthorizationCode: function () { - return true; - }, - saveToken: function () {}, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345, redirect_uri: "http://bar.foo" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - grantType.validateRedirectUri(request, authorizationCode); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid request: `redirect_uri` is invalid"); - } - }); - it("returns undefined and does not throw if `redirectUri` is valid", async function () { - const authorizationCode = { - authorizationCode: 12345, - client: {}, - expiresAt: new Date(new Date() / 2), - redirectUri: "http://foo.bar", - user: {}, - }; - const model = Model.from({ - getAuthorizationCode: function () {}, - revokeAuthorizationCode: function () { - return true; - }, - saveToken: function () {}, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345, redirect_uri: "http://foo.bar" }, - headers: {}, - method: {}, - query: {}, - }); - const value = grantType.validateRedirectUri(request, authorizationCode); - const isUndefined = value === undefined; - isUndefined.should.equal(true); - }); - }); - - describe("revokeAuthorizationCode()", function () { - it("should revoke the auth code", async function () { - const authorizationCode = { - authorizationCode: 12345, - client: {}, - expiresAt: new Date(new Date() / 2), - user: {}, - }; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: async function (_code) { - _code.should.equal(authorizationCode); - return true; - }, - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - - const data = await grantType.revokeAuthorizationCode(authorizationCode); - data.should.deep.equal(authorizationCode); - }); - - it("should throw an error when the auth code is invalid", async function () { - const authorizationCode = { - authorizationCode: 12345, - client: {}, - expiresAt: new Date(new Date() / 2), - user: {}, - }; - const returnTypes = [false, null, undefined, 0, ""]; - - for (const type of returnTypes) { - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: async function (_code) { - _code.should.equal(authorizationCode); - return type; - }, - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - - try { - await grantType.revokeAuthorizationCode(authorizationCode); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - "Invalid grant: authorization code is invalid", - ); - } - } - }); - - it("should support promises", function () { - const authorizationCode = { - authorizationCode: 12345, - client: {}, - expiresAt: new Date(new Date() / 2), - user: {}, - }; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: async function () { - return true; - }, - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - grantType - .revokeAuthorizationCode(authorizationCode) - .should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const authorizationCode = { - authorizationCode: 12345, - client: {}, - expiresAt: new Date(new Date() / 2), - user: {}, - }; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: function () { - return authorizationCode; - }, - saveToken: () => should.fail(), - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - grantType - .revokeAuthorizationCode(authorizationCode) - .should.be.an.instanceOf(Promise); - }); - }); - - describe("saveToken()", function () { - it("should save the token", async function () { - const token = { foo: "bar" }; - const model = Model.from({ - getAuthorizationCode: () => should.fail(), - revokeAuthorizationCode: () => should.fail(), - saveToken: function (_token, _client = "fallback", _user = "fallback") { - _token.accessToken.should.be.a.sha256(); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshToken.should.be.a.sha256(); - _token.scope.should.eql(["foo"]); - (_token.authorizationCode === undefined).should.equal(true); - _user.should.equal("fallback"); - _client.should.equal("fallback"); - return token; - }, - validateScope: function ( - _user = "fallback", - _client = "fallback", - _scope = ["fallback"], - ) { - _user.should.equal("fallback"); - _client.should.equal("fallback"); - _scope.should.eql(["fallback"]); - return ["foo"]; - }, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const data = await grantType.saveToken(); - data.should.equal(token); - }); - - it("should support promises", function () { - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () {}, - revokeAuthorizationCode: function () {}, - saveToken: async function () { - return token; - }, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () {}, - revokeAuthorizationCode: function () {}, - saveToken: function () { - return token; - }, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); +describe('AuthorizationCodeGrantType integration', function () { + describe('constructor()', function () { + it('should throw an error if `model` is missing', function () { + try { + new AuthorizationCodeGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getAuthorizationCode()`', function () { + try { + new AuthorizationCodeGrantType({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getAuthorizationCode()`', + ); + } + }); + + it('should throw an error if the model does not implement `revokeAuthorizationCode()`', function () { + try { + const model = Model.from({ + getAuthorizationCode: function () {}, + }); + + new AuthorizationCodeGrantType({ model: model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `revokeAuthorizationCode()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', function () { + try { + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () {}, + }); + + new AuthorizationCodeGrantType({ model: model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', function () { + it('should throw an error if `request` is missing', async function () { + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + try { + await grantType.handle(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is invalid (not in code)', async function () { + const client = { id: 1234 }; + const model = Model.from({ + getAuthorizationCode: function (code) { + code.should.equal(123456789); + return { + authorizationCode: 12345, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 123456789 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.handle(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAuthorizationCode()` did not return a `client` object', + ); + } + }); + + it('should throw an error if `client` is missing', function () { + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + grantType.handle(request, null); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', async function () { + const client = { id: 'foobar' }; + const scope = ['fooscope']; + const user = { name: 'foouser' }; + const codeDoc = { + authorizationCode: 12345, + expiresAt: new Date(new Date() * 2), + client, + user, + scope, + }; + const model = Model.from({ + getAuthorizationCode: async function (code) { + code.should.equal('code-1234'); + + return codeDoc; + }, + revokeAuthorizationCode: async function (_codeDoc) { + _codeDoc.should.deep.equal(codeDoc); + return true; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return 'long-access-token-hash'; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return 'long-refresh-token-hash'; + }, + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _token.accessToken.should.equal('long-access-token-hash'); + _token.refreshToken.should.equal('long-refresh-token-hash'); + _token.authorizationCode.should.equal(codeDoc.authorizationCode); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return _token; + }, + }); + + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 'code-1234' }, + headers: {}, + method: {}, + query: {}, + }); + + const token = await grantType.handle(request, client); + token.accessToken.should.equal('long-access-token-hash'); + token.refreshToken.should.equal('long-refresh-token-hash'); + token.authorizationCode.should.equal(codeDoc.authorizationCode); + token.accessTokenExpiresAt.should.be.instanceOf(Date); + token.refreshTokenExpiresAt.should.be.instanceOf(Date); + }); + + it('should support promises', function () { + const client = { id: 'foobar' }; + const model = Model.from({ + getAuthorizationCode: function () { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: function () { + return true; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const client = { id: 'foobar' }; + const model = Model.from({ + getAuthorizationCode: function () { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + revokeAuthorizationCode: function () { + return true; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + }); + + describe('getAuthorizationCode()', function () { + it('should throw an error if the request body does not contain `code`', async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `code`'); + } + }); + + it('should throw an error if `code` is invalid', async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 'øå€£‰' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `code`'); + } + }); + + it('should throw an error if `authorizationCode` is missing', async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: async function () {}, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code is invalid'); + } + }); + + it('should throw an error if `authorizationCode.client` is missing', async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: async function () { + return { authorizationCode: 12345 }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAuthorizationCode()` did not return a `client` object', + ); + } + }); + + it('should throw an error if `authorizationCode.expiresAt` is missing', async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: async function () { + return { authorizationCode: 12345, client: {}, user: {} }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `expiresAt` must be a Date instance', + ); + } + }); + + it('should throw an error if `authorizationCode.user` is missing', async function () { + const client = {}; + const model = Model.from({ + getAuthorizationCode: async function () { + return { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(), + }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAuthorizationCode()` did not return a `user` object', + ); + } + }); + + it('should throw an error if the client id does not match', async function () { + const client = { id: 123 }; + const model = Model.from({ + getAuthorizationCode: async function () { + return { + authorizationCode: 12345, + expiresAt: new Date(), + client: { id: 456 }, + user: {}, + }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code is invalid'); + } + }); + + it('should throw an error if the auth code is expired', async function () { + const client = { id: 123 }; + const date = new Date(new Date() / 2); + const model = Model.from({ + getAuthorizationCode: async function () { + return { + authorizationCode: 12345, + client: { id: 123 }, + expiresAt: date, + user: {}, + }; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: authorization code has expired'); + } + }); + + it('should throw an error if the `redirectUri` is invalid (format)', async function () { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() * 2), + redirectUri: 'foobar', + user: {}, + }; + const client = { id: 'foobar' }; + const model = Model.from({ + getAuthorizationCode: async function () { + return authorizationCode; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getAuthorizationCode(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: `redirect_uri` is not a valid URI', + ); + } + }); + + it('should return an auth code', async function () { + const authorizationCode = { + authorizationCode: 1234567, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + const client = { id: 'foobar' }; + const model = Model.from({ + getAuthorizationCode: async function (_code) { + _code.should.equal(12345); + return authorizationCode; + }, + revokeAuthorizationCode: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + const code = await grantType.getAuthorizationCode(request, client); + code.should.deep.equal(authorizationCode); + }); + + it('should support promises', function () { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + const client = { id: 'foobar' }; + const model = Model.from({ + getAuthorizationCode: async function () { + return authorizationCode; + }, + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + grantType + .getAuthorizationCode(request, client) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + const client = { id: 'foobar' }; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + grantType + .getAuthorizationCode(request, client) + .should.be.an.instanceOf(Promise); + }); + }); + + describe('validateRedirectUri()', function () { + it('should throw an error if `redirectUri` is missing', function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + redirectUri: 'http://foo.bar', + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + grantType.validateRedirectUri(request, authorizationCode); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + }); + + it('should throw an error if `redirectUri` is invalid', function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + redirectUri: 'http://foo.bar', + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () { + return true; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345, redirect_uri: 'http://bar.foo' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + grantType.validateRedirectUri(request, authorizationCode); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: `redirect_uri` is invalid'); + } + }); + it('returns undefined and does not throw if `redirectUri` is valid', async function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + redirectUri: 'http://foo.bar', + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () { + return true; + }, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345, redirect_uri: 'http://foo.bar' }, + headers: {}, + method: {}, + query: {}, + }); + const value = grantType.validateRedirectUri(request, authorizationCode); + const isUndefined = value === undefined; + isUndefined.should.equal(true); + }); + }); + + describe('revokeAuthorizationCode()', function () { + it('should revoke the auth code', async function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function (_code) { + _code.should.equal(authorizationCode); + return true; + }, + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + const data = await grantType.revokeAuthorizationCode(authorizationCode); + data.should.deep.equal(authorizationCode); + }); + + it('should throw an error when the auth code is invalid', async function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + const returnTypes = [false, null, undefined, 0, '']; + + for (const type of returnTypes) { + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function (_code) { + _code.should.equal(authorizationCode); + return type; + }, + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + try { + await grantType.revokeAuthorizationCode(authorizationCode); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: authorization code is invalid', + ); + } + } + }); + + it('should support promises', function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: async function () { + return true; + }, + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + grantType + .revokeAuthorizationCode(authorizationCode) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const authorizationCode = { + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: function () { + return authorizationCode; + }, + saveToken: () => should.fail(), + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + grantType + .revokeAuthorizationCode(authorizationCode) + .should.be.an.instanceOf(Promise); + }); + }); + + describe('saveToken()', function () { + it('should save the token', async function () { + const token = { foo: 'bar' }; + const model = Model.from({ + getAuthorizationCode: () => should.fail(), + revokeAuthorizationCode: () => should.fail(), + saveToken: function (_token, _client = 'fallback', _user = 'fallback') { + _token.accessToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshToken.should.be.a.sha256(); + _token.scope.should.eql(['foo']); + (_token.authorizationCode === undefined).should.equal(true); + _user.should.equal('fallback'); + _client.should.equal('fallback'); + return token; + }, + validateScope: function ( + _user = 'fallback', + _client = 'fallback', + _scope = ['fallback'], + ) { + _user.should.equal('fallback'); + _client.should.equal('fallback'); + _scope.should.eql(['fallback']); + return ['foo']; + }, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const data = await grantType.saveToken(); + data.should.equal(token); + }); + + it('should support promises', function () { + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () {}, + saveToken: async function () { + return token; + }, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () {}, + saveToken: function () { + return token; + }, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index 72ae028f..da548fd5 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -1,355 +1,355 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const ClientCredentialsGrantType = require("../../../lib/grant-types/client-credentials-grant-type"); -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); -const Model = require("../../../lib/model"); -const Request = require("../../../lib/request"); -const should = require("chai").should(); +const ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); +const Model = require('../../../lib/model'); +const Request = require('../../../lib/request'); +const should = require('chai').should(); /** * Test `ClientCredentialsGrantType` integration. */ -describe("ClientCredentialsGrantType integration", function () { - describe("constructor()", function () { - it("should throw an error if `model` is missing", function () { - try { - new ClientCredentialsGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `model`"); - } - }); - - it("should throw an error if the model does not implement `getUserFromClient()`", function () { - try { - new ClientCredentialsGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `getUserFromClient()`", - ); - } - }); - - it("should throw an error if the model does not implement `saveToken()`", function () { - try { - const model = Model.from({ - getUserFromClient: function () {}, - }); - - new ClientCredentialsGrantType({ model: model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `saveToken()`", - ); - } - }); - }); - - describe("handle()", function () { - it("should throw an error if `request` is missing", async function () { - const model = Model.from({ - getUserFromClient: function () {}, - saveToken: function () {}, - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - - try { - await grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `request`"); - } - }); - - it("should throw an error if `client` is missing", async function () { - const model = Model.from({ - getUserFromClient: function () {}, - saveToken: function () {}, - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `client`"); - } - }); - - it("should return a token", async function () { - const token = {}; - const client = { foo: "bar" }; - const user = { name: "foo" }; - const scope = ["fooscope"]; - - const model = Model.from({ - getUserFromClient: async function (_client) { - _client.should.deep.equal(client); - return { ...user }; - }, - saveToken: async function (_token, _client, _user) { - _client.should.deep.equal(client); - _user.should.deep.equal(user); - _token.accessToken.should.equal("long-access-token-hash"); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.scope.should.eql(scope); - return token; - }, - validateScope: async function (_user, _client, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return scope; - }, - generateAccessToken: async function (_client, _user, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return "long-access-token-hash"; - }, - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: { scope: scope.join(" ") }, - headers: {}, - method: {}, - query: {}, - }); - - const data = await grantType.handle(request, client); - data.should.equal(token); - }); - - it("should support promises", function () { - const token = {}; - const model = Model.from({ - getUserFromClient: async function () { - return {}; - }, - saveToken: async function () { - return token; - }, - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - grantType.handle(request, {}).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const token = {}; - const model = Model.from({ - getUserFromClient: function () { - return {}; - }, - saveToken: function () { - return token; - }, - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - grantType.handle(request, {}).should.be.an.instanceOf(Promise); - }); - }); - - describe("getUserFromClient()", function () { - it("should throw an error if `user` is missing", function () { - const model = Model.from({ - getUserFromClient: function () {}, - saveToken: () => should.fail(), - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - return grantType - .getUserFromClient(request, {}) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: user credentials are invalid"); - }); - }); - - it("should return a user", function () { - const user = { email: "foo@bar.com" }; - const model = Model.from({ - getUserFromClient: function () { - return user; - }, - saveToken: () => should.fail(), - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - return grantType - .getUserFromClient(request, {}) - .then(function (data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - - it("should support promises", function () { - const user = { email: "foo@bar.com" }; - const model = Model.from({ - getUserFromClient: async function () { - return user; - }, - saveToken: () => should.fail(), - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const user = { email: "foo@bar.com" }; - const model = Model.from({ - getUserFromClient: function () { - return user; - }, - saveToken: () => should.fail(), - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); - }); - }); - - describe("saveToken()", function () { - it("should save the token", async function () { - const token = {}; - const model = Model.from({ - getUserFromClient: () => should.fail(), - saveToken: function () { - return token; - }, - validateScope: function () { - return ["foo"]; - }, - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const data = await grantType.saveToken(token); - data.should.equal(token); - }); - - it("should support promises", function () { - const token = {}; - const model = Model.from({ - getUserFromClient: () => should.fail(), - saveToken: async function () { - return token; - }, - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 123, - model: model, - }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const token = {}; - const model = Model.from({ - getUserFromClient: () => should.fail(), - saveToken: function () { - return token; - }, - }); - const grantType = new ClientCredentialsGrantType({ - accessTokenLifetime: 123, - model: model, - }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); +describe('ClientCredentialsGrantType integration', function () { + describe('constructor()', function () { + it('should throw an error if `model` is missing', function () { + try { + new ClientCredentialsGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getUserFromClient()`', function () { + try { + new ClientCredentialsGrantType({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getUserFromClient()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', function () { + try { + const model = Model.from({ + getUserFromClient: function () {}, + }); + + new ClientCredentialsGrantType({ model: model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', function () { + it('should throw an error if `request` is missing', async function () { + const model = Model.from({ + getUserFromClient: function () {}, + saveToken: function () {}, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + + try { + await grantType.handle(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async function () { + const model = Model.from({ + getUserFromClient: function () {}, + saveToken: function () {}, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', async function () { + const token = {}; + const client = { foo: 'bar' }; + const user = { name: 'foo' }; + const scope = ['fooscope']; + + const model = Model.from({ + getUserFromClient: async function (_client) { + _client.should.deep.equal(client); + return { ...user }; + }, + saveToken: async function (_token, _client, _user) { + _client.should.deep.equal(client); + _user.should.deep.equal(user); + _token.accessToken.should.equal('long-access-token-hash'); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.scope.should.eql(scope); + return token; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return 'long-access-token-hash'; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { scope: scope.join(' ') }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await grantType.handle(request, client); + data.should.equal(token); + }); + + it('should support promises', function () { + const token = {}; + const model = Model.from({ + getUserFromClient: async function () { + return {}; + }, + saveToken: async function () { + return token; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, {}).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const token = {}; + const model = Model.from({ + getUserFromClient: function () { + return {}; + }, + saveToken: function () { + return token; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, {}).should.be.an.instanceOf(Promise); + }); + }); + + describe('getUserFromClient()', function () { + it('should throw an error if `user` is missing', function () { + const model = Model.from({ + getUserFromClient: function () {}, + saveToken: () => should.fail(), + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + return grantType + .getUserFromClient(request, {}) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + }); + }); + + it('should return a user', function () { + const user = { email: 'foo@bar.com' }; + const model = Model.from({ + getUserFromClient: function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + return grantType + .getUserFromClient(request, {}) + .then(function (data) { + data.should.equal(user); + }) + .catch(should.fail); + }); + + it('should support promises', function () { + const user = { email: 'foo@bar.com' }; + const model = Model.from({ + getUserFromClient: async function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const user = { email: 'foo@bar.com' }; + const model = Model.from({ + getUserFromClient: function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); + }); + }); + + describe('saveToken()', function () { + it('should save the token', async function () { + const token = {}; + const model = Model.from({ + getUserFromClient: () => should.fail(), + saveToken: function () { + return token; + }, + validateScope: function () { + return ['foo']; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const data = await grantType.saveToken(token); + data.should.equal(token); + }); + + it('should support promises', function () { + const token = {}; + const model = Model.from({ + getUserFromClient: () => should.fail(), + saveToken: async function () { + return token; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const token = {}; + const model = Model.from({ + getUserFromClient: () => should.fail(), + saveToken: function () { + return token; + }, + }); + const grantType = new ClientCredentialsGrantType({ + accessTokenLifetime: 123, + model: model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index 58757686..15673112 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -1,490 +1,490 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); -const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); -const PasswordGrantType = require("../../../lib/grant-types/password-grant-type"); -const Model = require("../../../lib/model"); -const Request = require("../../../lib/request"); -const should = require("chai").should(); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); +const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +const PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); +const Model = require('../../../lib/model'); +const Request = require('../../../lib/request'); +const should = require('chai').should(); /** * Test `PasswordGrantType` integration. */ -describe("PasswordGrantType integration", function () { - describe("constructor()", function () { - it("should throw an error if `model` is missing", function () { - try { - new PasswordGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `model`"); - } - }); - - it("should throw an error if the model does not implement `getUser()`", function () { - try { - new PasswordGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `getUser()`", - ); - } - }); - - it("should throw an error if the model does not implement `saveToken()`", function () { - try { - const model = Model.from({ - getUser: function () {}, - }); - - new PasswordGrantType({ model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `saveToken()`", - ); - } - }); - }); - - describe("handle()", function () { - it("should throw an error if `request` is missing", async function () { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - - try { - await grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `request`"); - } - }); - - it("should throw an error if `client` is missing", async function () { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - - try { - await grantType.handle({}); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `client`"); - } - }); - - it("should return a token", async function () { - const client = { id: "foobar" }; - const scope = ["baz"]; - const token = {}; - const user = { - id: 123456, - username: "foo", - email: "foo@example.com", - }; - - const model = Model.from({ - getUser: async function (username, password) { - username.should.equal("foo"); - password.should.equal("bar"); - return user; - }, - validateScope: async function (_user, _client, _scope) { - _client.should.equal(client); - _user.should.equal(user); - _scope.should.eql(scope); - return scope; - }, - generateAccessToken: async function (_client, _user, _scope) { - _client.should.equal(client); - _user.should.equal(user); - _scope.should.eql(scope); - return "long-access-token-hash"; - }, - generateRefreshToken: async function (_client, _user, _scope) { - _client.should.equal(client); - _user.should.equal(user); - _scope.should.eql(scope); - return "long-refresh-token-hash"; - }, - saveToken: async function (_token, _client, _user) { - _client.should.equal(client); - _user.should.equal(user); - _token.accessToken.should.equal("long-access-token-hash"); - _token.refreshToken.should.equal("long-refresh-token-hash"); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return token; - }, - }); - - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "foo", password: "bar", scope: "baz" }, - headers: {}, - method: {}, - query: {}, - }); - - const data = await grantType.handle(request, client); - data.should.equal(token); - }); - - it("should support promises", async function () { - const client = { id: "foobar" }; - const token = {}; - const model = Model.from({ - getUser: async function () { - return {}; - }, - saveToken: async function () { - return token; - }, - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "foo", password: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - - const result = await grantType.handle(request, client); - result.should.deep.equal({}); - }); - - it("should support non-promises", async function () { - const client = { id: "foobar" }; - const token = {}; - const model = Model.from({ - getUser: function () { - return {}; - }, - saveToken: function () { - return token; - }, - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "foo", password: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - - const result = await grantType.handle(request, client); - result.should.deep.equal({}); - }); - }); - - describe("getUser()", function () { - it("should throw an error if the request body does not contain `username`", async function () { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail(), - }); - const client = { id: "foobar" }; - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getUser(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Missing parameter: `username`"); - } - }); - - it("should throw an error if the request body does not contain `password`", async function () { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail(), - }); - const client = { id: "foobar" }; - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "foo" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getUser(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Missing parameter: `password`"); - } - }); - - it("should throw an error if `username` is invalid", async function () { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail(), - }); - const client = { id: "foobar" }; - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "\r\n", password: "foobar" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getUser(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `username`"); - } - }); - - it("should throw an error if `password` is invalid", async function () { - const model = Model.from({ - getUser: () => should.fail(), - saveToken: () => should.fail(), - }); - const client = { id: "foobar" }; - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "foobar", password: "\r\n" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getUser(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `password`"); - } - }); - - it("should throw an error if `user` is missing", async function () { - const model = Model.from({ - getUser: async () => undefined, - saveToken: () => should.fail(), - }); - const client = { id: "foobar" }; - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "foo", password: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getUser(request, client); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: user credentials are invalid"); - } - }); - - it("should return a user", async function () { - const user = { email: "foo@bar.com" }; - const client = { id: "foobar" }; - const model = Model.from({ - getUser: function (username, password) { - username.should.equal("foo"); - password.should.equal("bar"); - return user; - }, - saveToken: () => should.fail(), - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "foo", password: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - - const data = await grantType.getUser(request, client); - data.should.equal(user); - }); - - it("should support promises", function () { - const user = { email: "foo@bar.com" }; - const client = { id: "foobar" }; - const model = Model.from({ - getUser: async function () { - return user; - }, - saveToken: () => should.fail(), - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "foo", password: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - - grantType.getUser(request, client).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const user = { email: "foo@bar.com" }; - const client = { id: "foobar" }; - const model = Model.from({ - getUser: function () { - return user; - }, - saveToken: () => should.fail(), - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { username: "foo", password: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - - grantType.getUser(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe("saveToken()", function () { - it("should save the token", async function () { - const token = {}; - const model = Model.from({ - getUser: () => should.fail(), - saveToken: async function ( - _token, - _client = "fallback", - _user = "fallback", - ) { - _token.accessToken.should.be.a.sha256(); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshToken.should.be.a.sha256(); - _token.scope.should.eql(["foo"]); - _client.should.equal("fallback"); - _user.should.equal("fallback"); - return token; - }, - validateScope: async function (_scope = ["fallback"]) { - _scope.should.eql(["fallback"]); - return ["foo"]; - }, - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - - const data = await grantType.saveToken(); - data.should.equal(token); - }); - - it("should support promises", function () { - const token = {}; - const model = Model.from({ - getUser: () => should.fail(), - saveToken: async function () { - return token; - }, - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const token = {}; - const model = Model.from({ - getUser: () => should.fail(), - saveToken: function () { - return token; - }, - }); - const grantType = new PasswordGrantType({ - accessTokenLifetime: 123, - model, - }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); +describe('PasswordGrantType integration', function () { + describe('constructor()', function () { + it('should throw an error if `model` is missing', function () { + try { + new PasswordGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getUser()`', function () { + try { + new PasswordGrantType({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getUser()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', function () { + try { + const model = Model.from({ + getUser: function () {}, + }); + + new PasswordGrantType({ model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', function () { + it('should throw an error if `request` is missing', async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + try { + await grantType.handle({}); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', async function () { + const client = { id: 'foobar' }; + const scope = ['baz']; + const token = {}; + const user = { + id: 123456, + username: 'foo', + email: 'foo@example.com', + }; + + const model = Model.from({ + getUser: async function (username, password) { + username.should.equal('foo'); + password.should.equal('bar'); + return user; + }, + validateScope: async function (_user, _client, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.eql(scope); + return scope; + }, + generateAccessToken: async function (_client, _user, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.eql(scope); + return 'long-access-token-hash'; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _client.should.equal(client); + _user.should.equal(user); + _scope.should.eql(scope); + return 'long-refresh-token-hash'; + }, + saveToken: async function (_token, _client, _user) { + _client.should.equal(client); + _user.should.equal(user); + _token.accessToken.should.equal('long-access-token-hash'); + _token.refreshToken.should.equal('long-refresh-token-hash'); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + }, + }); + + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar', scope: 'baz' }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await grantType.handle(request, client); + data.should.equal(token); + }); + + it('should support promises', async function () { + const client = { id: 'foobar' }; + const token = {}; + const model = Model.from({ + getUser: async function () { + return {}; + }, + saveToken: async function () { + return token; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + + const result = await grantType.handle(request, client); + result.should.deep.equal({}); + }); + + it('should support non-promises', async function () { + const client = { id: 'foobar' }; + const token = {}; + const model = Model.from({ + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + + const result = await grantType.handle(request, client); + result.should.deep.equal({}); + }); + }); + + describe('getUser()', function () { + it('should throw an error if the request body does not contain `username`', async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `username`'); + } + }); + + it('should throw an error if the request body does not contain `password`', async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `password`'); + } + }); + + it('should throw an error if `username` is invalid', async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: '\r\n', password: 'foobar' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `username`'); + } + }); + + it('should throw an error if `password` is invalid', async function () { + const model = Model.from({ + getUser: () => should.fail(), + saveToken: () => should.fail(), + }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foobar', password: '\r\n' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `password`'); + } + }); + + it('should throw an error if `user` is missing', async function () { + const model = Model.from({ + getUser: async () => undefined, + saveToken: () => should.fail(), + }); + const client = { id: 'foobar' }; + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getUser(request, client); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + } + }); + + it('should return a user', async function () { + const user = { email: 'foo@bar.com' }; + const client = { id: 'foobar' }; + const model = Model.from({ + getUser: function (username, password) { + username.should.equal('foo'); + password.should.equal('bar'); + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await grantType.getUser(request, client); + data.should.equal(user); + }); + + it('should support promises', function () { + const user = { email: 'foo@bar.com' }; + const client = { id: 'foobar' }; + const model = Model.from({ + getUser: async function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.getUser(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const user = { email: 'foo@bar.com' }; + const client = { id: 'foobar' }; + const model = Model.from({ + getUser: function () { + return user; + }, + saveToken: () => should.fail(), + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.getUser(request, client).should.be.an.instanceOf(Promise); + }); + }); + + describe('saveToken()', function () { + it('should save the token', async function () { + const token = {}; + const model = Model.from({ + getUser: () => should.fail(), + saveToken: async function ( + _token, + _client = 'fallback', + _user = 'fallback', + ) { + _token.accessToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshToken.should.be.a.sha256(); + _token.scope.should.eql(['foo']); + _client.should.equal('fallback'); + _user.should.equal('fallback'); + return token; + }, + validateScope: async function (_scope = ['fallback']) { + _scope.should.eql(['fallback']); + return ['foo']; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + const data = await grantType.saveToken(); + data.should.equal(token); + }); + + it('should support promises', function () { + const token = {}; + const model = Model.from({ + getUser: () => should.fail(), + saveToken: async function () { + return token; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const token = {}; + const model = Model.from({ + getUser: () => should.fail(), + saveToken: function () { + return token; + }, + }); + const grantType = new PasswordGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index 188e38fe..93159ac6 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -1,791 +1,791 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); -const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); -const RefreshTokenGrantType = require("../../../lib/grant-types/refresh-token-grant-type"); -const Model = require("../../../lib/model"); -const Request = require("../../../lib/request"); -const ServerError = require("../../../lib/errors/server-error"); -const should = require("chai").should(); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); +const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +const RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); +const Model = require('../../../lib/model'); +const Request = require('../../../lib/request'); +const ServerError = require('../../../lib/errors/server-error'); +const should = require('chai').should(); /** * Test `RefreshTokenGrantType` integration. */ -describe("RefreshTokenGrantType integration", function () { - describe("constructor()", function () { - it("should throw an error if `model` is missing", function () { - try { - new RefreshTokenGrantType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `model`"); - } - }); - - it("should throw an error if the model does not implement `getRefreshToken()`", function () { - try { - new RefreshTokenGrantType({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `getRefreshToken()`", - ); - } - }); - - it("should throw an error if the model does not implement `revokeToken()`", function () { - try { - const model = Model.from({ - getRefreshToken: () => should.fail(), - }); - - new RefreshTokenGrantType({ model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `revokeToken()`", - ); - } - }); - - it("should throw an error if the model does not implement `saveToken()`", function () { - try { - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - }); - - new RefreshTokenGrantType({ model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `saveToken()`", - ); - } - }); - }); - - describe("handle()", function () { - it("should throw an error if `request` is missing", async function () { - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - - try { - await grantType.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `request`"); - } - }); - - it("should throw an error if `client` is missing", async function () { - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `client`"); - } - }); - - it("should return a token", async function () { - const client = { id: 123 }; - const token = { - accessToken: "foo", - client: { id: 123 }, - user: { name: "foo" }, - scope: ["read", "write"], - refreshTokenExpiresAt: new Date(new Date() * 2), - }; - const model = Model.from({ - getRefreshToken: async function (_refreshToken) { - _refreshToken.should.equal("foobar_refresh"); - return token; - }, - revokeToken: async function (_token) { - _token.should.deep.equal(token); - return true; - }, - generateAccessToken: async function (_client, _user, _scope) { - _user.should.deep.equal({ name: "foo" }); - _client.should.deep.equal({ id: 123 }); - _scope.should.eql(["read", "write"]); - return "new-access-token"; - }, - generateRefreshToken: async function (_client, _user, _scope) { - _user.should.deep.equal({ name: "foo" }); - _client.should.deep.equal({ id: 123 }); - _scope.should.eql(["read", "write"]); - return "new-refresh-token"; - }, - saveToken: async function (_token, _client, _user) { - _user.should.deep.equal({ name: "foo" }); - _client.should.deep.equal({ id: 123 }); - _token.accessToken.should.equal("new-access-token"); - _token.refreshToken.should.equal("new-refresh-token"); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return token; - }, - }); - - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { refresh_token: "foobar_refresh" }, - headers: {}, - method: {}, - query: {}, - }); - const data = await grantType.handle(request, client); - data.should.equal(token); - }); - - it("should support promises", function () { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function () { - return { accessToken: "foo", client: { id: 123 }, user: {} }; - }, - revokeToken: async function () { - return { - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }; - }, - saveToken: async function () { - return { accessToken: "foo", client: {}, user: {} }; - }, - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { refresh_token: "foobar" }, - headers: {}, - method: {}, - query: {}, - }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function () { - return { accessToken: "foo", client: { id: 123 }, user: {} }; - }, - revokeToken: async function () { - return { - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }; - }, - saveToken: async function () { - return { accessToken: "foo", client: {}, user: {} }; - }, - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { refresh_token: "foobar" }, - headers: {}, - method: {}, - query: {}, - }); - - grantType.handle(request, client).should.be.an.instanceOf(Promise); - }); - }); - - describe("getRefreshToken()", function () { - it("should throw an error if the `refreshToken` parameter is missing from the request body", async function () { - const client = {}; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Missing parameter: `refresh_token`"); - } - }); - - it("should throw an error if `refreshToken` is not found", async function () { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function () {}, - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: { refresh_token: "12345" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: refresh token is invalid"); - } - }); - - it("should throw an error if `refreshToken.client` is missing", async function () { - const client = {}; - const model = Model.from({ - getRefreshToken: async function () { - return {}; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: { refresh_token: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - "Server error: `getRefreshToken()` did not return a `client` object", - ); - } - }); - - it("should throw an error if `refreshToken.user` is missing", async function () { - const client = {}; - const model = Model.from({ - getRefreshToken: async function () { - return { accessToken: "foo", client: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: { refresh_token: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - "Server error: `getRefreshToken()` did not return a `user` object", - ); - } - }); - - it("should throw an error if the client id does not match", async function () { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function () { - return { accessToken: "foo", client: { id: 456 }, user: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: { refresh_token: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - "Invalid grant: refresh token was issued to another client", - ); - } - }); - - it("should throw an error if `refresh_token` contains invalid characters", async function () { - const client = {}; - const model = Model.from({ - getRefreshToken: async function () { - return { client: { id: 456 }, user: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: { refresh_token: "øå€£‰" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `refresh_token`"); - } - }); - - it("should throw an error if `refresh_token` is missing", async function () { - const client = {}; - const model = Model.from({ - getRefreshToken: async function () { - return { accessToken: "foo", client: { id: 456 }, user: {} }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: { refresh_token: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - "Invalid grant: refresh token was issued to another client", - ); - } - }); - - it("should throw an error if `refresh_token` is expired", async function () { - const client = { id: 123 }; - const date = new Date(new Date() / 2); - const model = Model.from({ - getRefreshToken: async function () { - return { - accessToken: "foo", - client: { id: 123 }, - refreshTokenExpiresAt: date, - user: {}, - }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: { refresh_token: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: refresh token has expired"); - } - }); - - it("should throw an error if `refreshTokenExpiresAt` is not a date value", async function () { - const client = { id: 123 }; - const model = Model.from({ - getRefreshToken: async function () { - return { - accessToken: "foo", - client: { id: 123 }, - refreshTokenExpiresAt: "stringvalue", - user: {}, - }; - }, - revokeToken: () => should.fail(), - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - const request = new Request({ - body: { refresh_token: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await grantType.getRefreshToken(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - "Server error: `refreshTokenExpiresAt` must be a Date instance", - ); - } - }); - - it("should return a token", async function () { - const client = { id: 123 }; - const token = { - accessToken: "foo", - client: { id: 123 }, - user: { name: "foobar" }, - }; - const model = Model.from({ - getRefreshToken: async function (_refreshToken) { - _refreshToken.should.equal("foobar_refresh"); - return token; - }, - revokeToken: async function (_token) { - _token.should.deep.equal(token); - return true; - }, - saveToken: async function (_token, _client, _user) { - _user.should.deep.equal(token.user); - _client.should.deep.equal(client); - _token.accessToken.should.be.a.sha256(); - _token.refreshToken.should.be.a.sha256(); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return token; - }, - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { refresh_token: "foobar_refresh" }, - headers: {}, - method: {}, - query: {}, - }); - - const data = await grantType.getRefreshToken(request, client); - data.should.equal(token); - }); - - it("should support promises", function () { - const client = { id: 123 }; - const token = { accessToken: "foo", client: { id: 123 }, user: {} }; - const model = Model.from({ - getRefreshToken: async function () { - return token; - }, - revokeToken: async function () {}, - saveToken: async function () {}, - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { refresh_token: "foobar" }, - headers: {}, - method: {}, - query: {}, - }); - - grantType - .getRefreshToken(request, client) - .should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const client = { id: 123 }; - const token = { accessToken: "foo", client: { id: 123 }, user: {} }; - const model = Model.from({ - getRefreshToken: async function () { - return token; - }, - revokeToken: async function () {}, - saveToken: async function () {}, - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - const request = new Request({ - body: { refresh_token: "foobar" }, - headers: {}, - method: {}, - query: {}, - }); - - grantType - .getRefreshToken(request, client) - .should.be.an.instanceOf(Promise); - }); - }); - - describe("revokeToken()", function () { - it("should throw an error if the `token` is invalid", async function () { - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: async () => {}, - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model, - }); - - try { - await grantType.revokeToken({}); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - "Invalid grant: refresh token is invalid or could not be revoked", - ); - } - }); - - it("should revoke the token", async function () { - const token = { - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: async function (_token) { - _token.should.deep.equal(token); - return token; - }, - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - - const data = await grantType.revokeToken(token); - data.should.equal(token); - }); - - it("should support promises", function () { - const token = { - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: async function () { - return token; - }, - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const token = { - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: function () { - return token; - }, - saveToken: () => should.fail(), - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - - grantType.revokeToken(token).should.be.an.instanceOf(Promise); - }); - }); - - describe("saveToken()", function () { - it("should save the token", async function () { - const user = { name: "foo" }; - const client = { id: 123465 }; - const scope = ["foo", "bar"]; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: async function (_token, _client, _user) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _token.scope.should.deep.eql(scope); - _token.accessToken.should.be.a.sha256(); - _token.refreshToken.should.be.a.sha256(); - _token.accessTokenExpiresAt.should.be.instanceOf(Date); - _token.refreshTokenExpiresAt.should.be.instanceOf(Date); - return { ..._token }; - }, - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - - const data = await grantType.saveToken(user, client, scope); - data.accessToken.should.be.a.sha256(); - data.refreshToken.should.be.a.sha256(); - data.accessTokenExpiresAt.should.be.instanceOf(Date); - data.refreshTokenExpiresAt.should.be.instanceOf(Date); - data.scope.should.deep.equal(scope); - }); - - it("should support promises", function () { - const token = {}; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: async function () { - return token; - }, - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const token = {}; - const model = Model.from({ - getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), - saveToken: function () { - return token; - }, - }); - const grantType = new RefreshTokenGrantType({ - accessTokenLifetime: 123, - model, - }); - - grantType.saveToken(token).should.be.an.instanceOf(Promise); - }); - }); +describe('RefreshTokenGrantType integration', function () { + describe('constructor()', function () { + it('should throw an error if `model` is missing', function () { + try { + new RefreshTokenGrantType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getRefreshToken()`', function () { + try { + new RefreshTokenGrantType({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getRefreshToken()`', + ); + } + }); + + it('should throw an error if the model does not implement `revokeToken()`', function () { + try { + const model = Model.from({ + getRefreshToken: () => should.fail(), + }); + + new RefreshTokenGrantType({ model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `revokeToken()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveToken()`', function () { + try { + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + }); + + new RefreshTokenGrantType({ model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveToken()`', + ); + } + }); + }); + + describe('handle()', function () { + it('should throw an error if `request` is missing', async function () { + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + try { + await grantType.handle(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `request`'); + } + }); + + it('should throw an error if `client` is missing', async function () { + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + + it('should return a token', async function () { + const client = { id: 123 }; + const token = { + accessToken: 'foo', + client: { id: 123 }, + user: { name: 'foo' }, + scope: ['read', 'write'], + refreshTokenExpiresAt: new Date(new Date() * 2), + }; + const model = Model.from({ + getRefreshToken: async function (_refreshToken) { + _refreshToken.should.equal('foobar_refresh'); + return token; + }, + revokeToken: async function (_token) { + _token.should.deep.equal(token); + return true; + }, + generateAccessToken: async function (_client, _user, _scope) { + _user.should.deep.equal({ name: 'foo' }); + _client.should.deep.equal({ id: 123 }); + _scope.should.eql(['read', 'write']); + return 'new-access-token'; + }, + generateRefreshToken: async function (_client, _user, _scope) { + _user.should.deep.equal({ name: 'foo' }); + _client.should.deep.equal({ id: 123 }); + _scope.should.eql(['read', 'write']); + return 'new-refresh-token'; + }, + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal({ name: 'foo' }); + _client.should.deep.equal({ id: 123 }); + _token.accessToken.should.equal('new-access-token'); + _token.refreshToken.should.equal('new-refresh-token'); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + }, + }); + + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar_refresh' }, + headers: {}, + method: {}, + query: {}, + }); + const data = await grantType.handle(request, client); + data.should.equal(token); + }); + + it('should support promises', function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: 'foo', client: { id: 123 }, user: {} }; + }, + revokeToken: async function () { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + saveToken: async function () { + return { accessToken: 'foo', client: {}, user: {} }; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: 'foo', client: { id: 123 }, user: {} }; + }, + revokeToken: async function () { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + saveToken: async function () { + return { accessToken: 'foo', client: {}, user: {} }; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: {}, + query: {}, + }); + + grantType.handle(request, client).should.be.an.instanceOf(Promise); + }); + }); + + describe('getRefreshToken()', function () { + it('should throw an error if the `refreshToken` parameter is missing from the request body', async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `refresh_token`'); + } + }); + + it('should throw an error if `refreshToken` is not found', async function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () {}, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: '12345' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token is invalid'); + } + }); + + it('should throw an error if `refreshToken.client` is missing', async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: async function () { + return {}; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getRefreshToken()` did not return a `client` object', + ); + } + }); + + it('should throw an error if `refreshToken.user` is missing', async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: 'foo', client: {} }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getRefreshToken()` did not return a `user` object', + ); + } + }); + + it('should throw an error if the client id does not match', async function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: 'foo', client: { id: 456 }, user: {} }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: refresh token was issued to another client', + ); + } + }); + + it('should throw an error if `refresh_token` contains invalid characters', async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: async function () { + return { client: { id: 456 }, user: {} }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 'øå€£‰' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `refresh_token`'); + } + }); + + it('should throw an error if `refresh_token` is missing', async function () { + const client = {}; + const model = Model.from({ + getRefreshToken: async function () { + return { accessToken: 'foo', client: { id: 456 }, user: {} }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: refresh token was issued to another client', + ); + } + }); + + it('should throw an error if `refresh_token` is expired', async function () { + const client = { id: 123 }; + const date = new Date(new Date() / 2); + const model = Model.from({ + getRefreshToken: async function () { + return { + accessToken: 'foo', + client: { id: 123 }, + refreshTokenExpiresAt: date, + user: {}, + }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: refresh token has expired'); + } + }); + + it('should throw an error if `refreshTokenExpiresAt` is not a date value', async function () { + const client = { id: 123 }; + const model = Model.from({ + getRefreshToken: async function () { + return { + accessToken: 'foo', + client: { id: 123 }, + refreshTokenExpiresAt: 'stringvalue', + user: {}, + }; + }, + revokeToken: () => should.fail(), + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + const request = new Request({ + body: { refresh_token: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await grantType.getRefreshToken(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `refreshTokenExpiresAt` must be a Date instance', + ); + } + }); + + it('should return a token', async function () { + const client = { id: 123 }; + const token = { + accessToken: 'foo', + client: { id: 123 }, + user: { name: 'foobar' }, + }; + const model = Model.from({ + getRefreshToken: async function (_refreshToken) { + _refreshToken.should.equal('foobar_refresh'); + return token; + }, + revokeToken: async function (_token) { + _token.should.deep.equal(token); + return true; + }, + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal(token.user); + _client.should.deep.equal(client); + _token.accessToken.should.be.a.sha256(); + _token.refreshToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return token; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar_refresh' }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await grantType.getRefreshToken(request, client); + data.should.equal(token); + }); + + it('should support promises', function () { + const client = { id: 123 }; + const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = Model.from({ + getRefreshToken: async function () { + return token; + }, + revokeToken: async function () {}, + saveToken: async function () {}, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: {}, + query: {}, + }); + + grantType + .getRefreshToken(request, client) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const client = { id: 123 }; + const token = { accessToken: 'foo', client: { id: 123 }, user: {} }; + const model = Model.from({ + getRefreshToken: async function () { + return token; + }, + revokeToken: async function () {}, + saveToken: async function () {}, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar' }, + headers: {}, + method: {}, + query: {}, + }); + + grantType + .getRefreshToken(request, client) + .should.be.an.instanceOf(Promise); + }); + }); + + describe('revokeToken()', function () { + it('should throw an error if the `token` is invalid', async function () { + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: async () => {}, + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model, + }); + + try { + await grantType.revokeToken({}); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal( + 'Invalid grant: refresh token is invalid or could not be revoked', + ); + } + }); + + it('should revoke the token', async function () { + const token = { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: async function (_token) { + _token.should.deep.equal(token); + return token; + }, + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + const data = await grantType.revokeToken(token); + data.should.equal(token); + }); + + it('should support promises', function () { + const token = { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: async function () { + return token; + }, + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const token = { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: function () { + return token; + }, + saveToken: () => should.fail(), + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.revokeToken(token).should.be.an.instanceOf(Promise); + }); + }); + + describe('saveToken()', function () { + it('should save the token', async function () { + const user = { name: 'foo' }; + const client = { id: 123465 }; + const scope = ['foo', 'bar']; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: async function (_token, _client, _user) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _token.scope.should.deep.eql(scope); + _token.accessToken.should.be.a.sha256(); + _token.refreshToken.should.be.a.sha256(); + _token.accessTokenExpiresAt.should.be.instanceOf(Date); + _token.refreshTokenExpiresAt.should.be.instanceOf(Date); + return { ..._token }; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + const data = await grantType.saveToken(user, client, scope); + data.accessToken.should.be.a.sha256(); + data.refreshToken.should.be.a.sha256(); + data.accessTokenExpiresAt.should.be.instanceOf(Date); + data.refreshTokenExpiresAt.should.be.instanceOf(Date); + data.scope.should.deep.equal(scope); + }); + + it('should support promises', function () { + const token = {}; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: async function () { + return token; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const token = {}; + const model = Model.from({ + getRefreshToken: () => should.fail(), + revokeToken: () => should.fail(), + saveToken: function () { + return token; + }, + }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + + grantType.saveToken(token).should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index 6d7728f1..b67b41bf 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -1,906 +1,906 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AccessDeniedError = require("../../../lib/errors/access-denied-error"); -const AuthenticateHandler = require("../../../lib/handlers/authenticate-handler"); -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); -const InsufficientScopeError = require("../../../lib/errors/insufficient-scope-error"); -const InvalidTokenError = require("../../../lib/errors/invalid-token-error"); -const Model = require("../../../lib/model"); -const Request = require("../../../lib/request"); -const Response = require("../../../lib/response"); -const ServerError = require("../../../lib/errors/server-error"); -const UnauthorizedRequestError = require("../../../lib/errors/unauthorized-request-error"); -const should = require("chai").should(); +const AccessDeniedError = require('../../../lib/errors/access-denied-error'); +const AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +const InsufficientScopeError = require('../../../lib/errors/insufficient-scope-error'); +const InvalidTokenError = require('../../../lib/errors/invalid-token-error'); +const Model = require('../../../lib/model'); +const Request = require('../../../lib/request'); +const Response = require('../../../lib/response'); +const ServerError = require('../../../lib/errors/server-error'); +const UnauthorizedRequestError = require('../../../lib/errors/unauthorized-request-error'); +const should = require('chai').should(); /** * Test `AuthenticateHandler` integration. */ -describe("AuthenticateHandler integration", function () { - describe("constructor()", function () { - it("should throw an error if `options.model` is missing", function () { - try { - new AuthenticateHandler(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `model`"); - } - }); - - it("should throw an error if the model does not implement `getAccessToken()`", function () { - try { - new AuthenticateHandler({ model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `getAccessToken()`", - ); - } - }); - - it("should throw an error if `scope` was given and `addAcceptedScopesHeader()` is missing", function () { - try { - new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - scope: ["foobar"], - }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `addAcceptedScopesHeader`"); - } - }); - - it("should throw an error if `scope` was given and `addAuthorizedScopesHeader()` is missing", function () { - try { - new AuthenticateHandler({ - addAcceptedScopesHeader: true, - model: { getAccessToken: function () {} }, - scope: ["foobar"], - }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Missing parameter: `addAuthorizedScopesHeader`", - ); - } - }); - - it("should throw an error if `scope` was given and the model does not implement `verifyScope()`", function () { - try { - new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: { getAccessToken: function () {} }, - scope: ["foobar"], - }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `verifyScope()`", - ); - } - }); - - it("should set the `model`", function () { - const model = Model.from({ getAccessToken: function () {} }); - const grantType = new AuthenticateHandler({ model: model }); - - grantType.model.should.equal(model); - }); - - it("should set the `scope`", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () {}, - }); - const grantType = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: "foobar", - }); - - grantType.scope.should.eql(["foobar"]); - }); - }); - - describe("handle()", function () { - it("should throw an error if `request` is missing or not a Request instance", async function () { - class Request {} // intentionally fake - const values = [undefined, null, {}, [], new Date(), new Request()]; - for (const request of values) { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - - try { - await handler.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: `request` must be an instance of Request", - ); - } - } - }); - - it("should throw an error if `response` is missing or not a Response instance", async function () { - class Response {} // intentionally fake - const values = [undefined, null, {}, [], new Date(), new Response()]; - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - - for (const response of values) { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - try { - await handler.handle(request, response); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: `response` must be an instance of Response", - ); - } - } - }); - - it("should set the `WWW-Authenticate` header if an unauthorized request error is thrown", async function () { - const model = Model.from({ - getAccessToken: function () { - throw new UnauthorizedRequestError(); - }, - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - response.get("WWW-Authenticate").should.equal('Bearer realm="Service"'); - } - }); - - it("should set the `WWW-Authenticate` header if an InvalidRequestError is thrown", function () { - const model = Model.from({ - getAccessToken: function () { - throw new InvalidRequestError(); - }, - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function () { - response - .get("WWW-Authenticate") - .should.equal('Bearer realm="Service",error="invalid_request"'); - }); - }); - - it("should set the `WWW-Authenticate` header if an InvalidTokenError is thrown", function () { - const model = Model.from({ - getAccessToken: function () { - throw new InvalidTokenError(); - }, - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function () { - response - .get("WWW-Authenticate") - .should.equal('Bearer realm="Service",error="invalid_token"'); - }); - }); - - it("should set the `WWW-Authenticate` header if an InsufficientScopeError is thrown", function () { - const model = Model.from({ - getAccessToken: function () { - throw new InsufficientScopeError(); - }, - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function () { - response - .get("WWW-Authenticate") - .should.equal('Bearer realm="Service",error="insufficient_scope"'); - }); - }); - - it("should throw the error if an oauth error is thrown", function () { - const model = Model.from({ - getAccessToken: function () { - throw new AccessDeniedError("Cannot request this access token"); - }, - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal("Cannot request this access token"); - }); - }); - - it("should throw a server error if a non-oauth error is thrown", function () { - const model = Model.from({ - getAccessToken: function () { - throw new Error("Unhandled exception"); - }, - }); - const handler = new AuthenticateHandler({ model: model }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal("Unhandled exception"); - }); - }); - - it("should return an access token", function () { - const accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - const model = Model.from({ - getAccessToken: function () { - return accessToken; - }, - verifyScope: function () { - return true; - }, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: ["foo"], - }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(function (data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - - it("should return an access token (deprecated)", function () { - const accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - const model = Model.from({ - getAccessToken: function () { - return accessToken; - }, - verifyScope: function () { - return true; - }, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: "foo", - }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(function (data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - }); - - describe("getTokenFromRequest()", function () { - it("should throw an error if more than one authentication method is used", async function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: { access_token: "foo" }, - }); - - try { - await handler.getTokenFromRequest(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: only one authentication method is allowed", - ); - } - }); - - it("should throw an error if `accessToken` is missing", async function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getTokenFromRequest(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnauthorizedRequestError); - e.message.should.equal("Unauthorized request: no authentication given"); - } - }); - }); - - describe("getTokenFromRequestHeader()", function () { - it("should throw an error if the token is malformed", async function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: {}, - headers: { - Authorization: "foobar", - }, - method: {}, - query: {}, - }); - - try { - await handler.getTokenFromRequestHeader(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: malformed authorization header", - ); - } - }); - - it("should return the bearer token", function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: {}, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: {}, - }); - - const bearerToken = handler.getTokenFromRequestHeader(request); - - bearerToken.should.equal("foo"); - }); - }); - - describe("getTokenFromRequestQuery()", function () { - it("should throw an error if the query contains a token", async function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - - try { - await handler.getTokenFromRequestQuery(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: do not send bearer tokens in query URLs", - ); - } - }); - - it("should return the bearer token if `allowBearerTokensInQueryString` is true", function () { - const handler = new AuthenticateHandler({ - allowBearerTokensInQueryString: true, - model: { getAccessToken: function () {} }, - }); - - handler - .getTokenFromRequestQuery({ query: { access_token: "foo" } }) - .should.equal("foo"); - }); - }); - - describe("getTokenFromRequestBody()", function () { - it("should throw an error if the method is `GET`", async function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: { access_token: "foo" }, - headers: {}, - method: "GET", - query: {}, - }); - - try { - await handler.getTokenFromRequestBody(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: token may not be passed in the body when using the GET verb", - ); - } - }); - - it("should throw an error if the media type is not `application/x-www-form-urlencoded`", async function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: { access_token: "foo" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getTokenFromRequestBody(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: content must be application/x-www-form-urlencoded", - ); - } - }); - - it("should return the bearer token", function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: { access_token: "foo" }, - headers: { - "content-type": "application/x-www-form-urlencoded", - "transfer-encoding": "chunked", - }, - method: {}, - query: {}, - }); - - handler.getTokenFromRequestBody(request).should.equal("foo"); - }); - }); - - describe("getAccessToken()", function () { - it("should throw an error if `accessToken` is missing", function () { - const model = Model.from({ - getAccessToken: function () {}, - }); - const handler = new AuthenticateHandler({ model: model }); - - return handler - .getAccessToken("foo") - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal("Invalid token: access token is invalid"); - }); - }); - - it("should throw an error if `accessToken.user` is missing", function () { - const model = Model.from({ - getAccessToken: function () { - return {}; - }, - }); - const handler = new AuthenticateHandler({ model: model }); - - return handler - .getAccessToken("foo") - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - "Server error: `getAccessToken()` did not return a `user` object", - ); - }); - }); - - it("should return an access token", function () { - const accessToken = { user: {} }; - const model = Model.from({ - getAccessToken: function () { - return accessToken; - }, - }); - const handler = new AuthenticateHandler({ model: model }); - - return handler - .getAccessToken("foo") - .then(function (data) { - data.should.equal(accessToken); - }) - .catch(should.fail); - }); - - it("should support promises", function () { - const model = Model.from({ - getAccessToken: async function () { - return { user: {} }; - }, - }); - const handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken("foo").should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const model = Model.from({ - getAccessToken: function () { - return { user: {} }; - }, - }); - const handler = new AuthenticateHandler({ model: model }); - - handler.getAccessToken("foo").should.be.an.instanceOf(Promise); - }); - }); - - describe("validateAccessToken()", function () { - it("should throw an error if `accessToken` is expired", async function () { - const accessToken = { accessTokenExpiresAt: new Date(new Date() / 2) }; - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - - try { - await handler.validateAccessToken(accessToken); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidTokenError); - e.message.should.equal("Invalid token: access token has expired"); - } - }); - - it("should return an access token", function () { - const accessToken = { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - - handler.validateAccessToken(accessToken).should.equal(accessToken); - }); - }); - - describe("verifyScope()", function () { - it("should throw an error if `scope` is insufficient (deprecated)", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () { - return false; - }, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: "foo", - }); - - return handler - .verifyScope(["foo"]) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InsufficientScopeError); - e.message.should.equal( - "Insufficient scope: authorized scope is insufficient", - ); - }); - }); - - it("should throw an error if `scope` is insufficient", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () { - return false; - }, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: ["foo"], - }); - - return handler - .verifyScope(["foo"]) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InsufficientScopeError); - e.message.should.equal( - "Insufficient scope: authorized scope is insufficient", - ); - }); - }); - - it("should support promises (deprecated)", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () { - return true; - }, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: "foo", - }); - - handler.verifyScope(["foo"]).should.be.an.instanceOf(Promise); - }); - - it("should support promises", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () { - return true; - }, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: ["foo"], - }); - - handler.verifyScope(["foo"]).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises (deprecated)", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () { - return true; - }, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: "foo", - }); - - handler.verifyScope(["foo"]).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () { - return true; - }, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: ["foo"], - }); - - handler.verifyScope(["foo"]).should.be.an.instanceOf(Promise); - }); - }); - - describe("updateResponse()", function () { - it("should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () {}, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: false, - model: model, - }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ["foo", "biz"] }); - - response.headers.should.not.have.property("x-accepted-oauth-scopes"); - }); - - it("should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified (deprecated)", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () {}, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: false, - model: model, - scope: "foo bar", - }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ["foo", "biz"] }); - - response.get("X-Accepted-OAuth-Scopes").should.equal("foo bar"); - }); - - it("should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () {}, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: false, - model: model, - scope: ["foo", "bar"], - }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ["foo", "biz"] }); - - response.get("X-Accepted-OAuth-Scopes").should.equal("foo bar"); - }); - - it("should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () {}, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: false, - addAuthorizedScopesHeader: true, - model: model, - }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ["foo", "biz"] }); - - response.headers.should.not.have.property("x-oauth-scopes"); - }); - - it("should set the `X-Authorized-OAuth-Scopes` header (deprecated)", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () {}, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: false, - addAuthorizedScopesHeader: true, - model: model, - scope: "foo bar", - }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ["foo", "biz"] }); - - response.get("X-OAuth-Scopes").should.equal("foo biz"); - }); - - it("should set the `X-Authorized-OAuth-Scopes` header", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: function () {}, - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: false, - addAuthorizedScopesHeader: true, - model: model, - scope: ["foo", "bar"], - }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateResponse(response, { scope: ["foo", "biz"] }); - - response.get("X-OAuth-Scopes").should.equal("foo biz"); - }); - }); +describe('AuthenticateHandler integration', function () { + describe('constructor()', function () { + it('should throw an error if `options.model` is missing', function () { + try { + new AuthenticateHandler(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getAccessToken()`', function () { + try { + new AuthenticateHandler({ model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + }); + + it('should throw an error if `scope` was given and `addAcceptedScopesHeader()` is missing', function () { + try { + new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + scope: ['foobar'], + }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `addAcceptedScopesHeader`'); + } + }); + + it('should throw an error if `scope` was given and `addAuthorizedScopesHeader()` is missing', function () { + try { + new AuthenticateHandler({ + addAcceptedScopesHeader: true, + model: { getAccessToken: function () {} }, + scope: ['foobar'], + }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Missing parameter: `addAuthorizedScopesHeader`', + ); + } + }); + + it('should throw an error if `scope` was given and the model does not implement `verifyScope()`', function () { + try { + new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: { getAccessToken: function () {} }, + scope: ['foobar'], + }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `verifyScope()`', + ); + } + }); + + it('should set the `model`', function () { + const model = Model.from({ getAccessToken: function () {} }); + const grantType = new AuthenticateHandler({ model: model }); + + grantType.model.should.equal(model); + }); + + it('should set the `scope`', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const grantType = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: 'foobar', + }); + + grantType.scope.should.eql(['foobar']); + }); + }); + + describe('handle()', function () { + it('should throw an error if `request` is missing or not a Request instance', async function () { + class Request {} // intentionally fake + const values = [undefined, null, {}, [], new Date(), new Request()]; + for (const request of values) { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + + try { + await handler.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + } + }); + + it('should throw an error if `response` is missing or not a Response instance', async function () { + class Response {} // intentionally fake + const values = [undefined, null, {}, [], new Date(), new Response()]; + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + + for (const response of values) { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + try { + await handler.handle(request, response); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `response` must be an instance of Response', + ); + } + } + }); + + it('should set the `WWW-Authenticate` header if an unauthorized request error is thrown', async function () { + const model = Model.from({ + getAccessToken: function () { + throw new UnauthorizedRequestError(); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + response.get('WWW-Authenticate').should.equal('Bearer realm="Service"'); + } + }); + + it('should set the `WWW-Authenticate` header if an InvalidRequestError is thrown', function () { + const model = Model.from({ + getAccessToken: function () { + throw new InvalidRequestError(); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function () { + response + .get('WWW-Authenticate') + .should.equal('Bearer realm="Service",error="invalid_request"'); + }); + }); + + it('should set the `WWW-Authenticate` header if an InvalidTokenError is thrown', function () { + const model = Model.from({ + getAccessToken: function () { + throw new InvalidTokenError(); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function () { + response + .get('WWW-Authenticate') + .should.equal('Bearer realm="Service",error="invalid_token"'); + }); + }); + + it('should set the `WWW-Authenticate` header if an InsufficientScopeError is thrown', function () { + const model = Model.from({ + getAccessToken: function () { + throw new InsufficientScopeError(); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function () { + response + .get('WWW-Authenticate') + .should.equal('Bearer realm="Service",error="insufficient_scope"'); + }); + }); + + it('should throw the error if an oauth error is thrown', function () { + const model = Model.from({ + getAccessToken: function () { + throw new AccessDeniedError('Cannot request this access token'); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal('Cannot request this access token'); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', function () { + const model = Model.from({ + getAccessToken: function () { + throw new Error('Unhandled exception'); + }, + }); + const handler = new AuthenticateHandler({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + }); + }); + + it('should return an access token', function () { + const accessToken = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const model = Model.from({ + getAccessToken: function () { + return accessToken; + }, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: ['foo'], + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function (data) { + data.should.equal(accessToken); + }) + .catch(should.fail); + }); + + it('should return an access token (deprecated)', function () { + const accessToken = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const model = Model.from({ + getAccessToken: function () { + return accessToken; + }, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: 'foo', + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function (data) { + data.should.equal(accessToken); + }) + .catch(should.fail); + }); + }); + + describe('getTokenFromRequest()', function () { + it('should throw an error if more than one authentication method is used', async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: { access_token: 'foo' }, + }); + + try { + await handler.getTokenFromRequest(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: only one authentication method is allowed', + ); + } + }); + + it('should throw an error if `accessToken` is missing', async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getTokenFromRequest(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnauthorizedRequestError); + e.message.should.equal('Unauthorized request: no authentication given'); + } + }); + }); + + describe('getTokenFromRequestHeader()', function () { + it('should throw an error if the token is malformed', async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: 'foobar', + }, + method: {}, + query: {}, + }); + + try { + await handler.getTokenFromRequestHeader(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: malformed authorization header', + ); + } + }); + + it('should return the bearer token', function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: {}, + }); + + const bearerToken = handler.getTokenFromRequestHeader(request); + + bearerToken.should.equal('foo'); + }); + }); + + describe('getTokenFromRequestQuery()', function () { + it('should throw an error if the query contains a token', async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + + try { + await handler.getTokenFromRequestQuery(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: do not send bearer tokens in query URLs', + ); + } + }); + + it('should return the bearer token if `allowBearerTokensInQueryString` is true', function () { + const handler = new AuthenticateHandler({ + allowBearerTokensInQueryString: true, + model: { getAccessToken: function () {} }, + }); + + handler + .getTokenFromRequestQuery({ query: { access_token: 'foo' } }) + .should.equal('foo'); + }); + }); + + describe('getTokenFromRequestBody()', function () { + it('should throw an error if the method is `GET`', async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: {}, + method: 'GET', + query: {}, + }); + + try { + await handler.getTokenFromRequestBody(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: token may not be passed in the body when using the GET verb', + ); + } + }); + + it('should throw an error if the media type is not `application/x-www-form-urlencoded`', async function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getTokenFromRequestBody(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + } + }); + + it('should return the bearer token', function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: {}, + query: {}, + }); + + handler.getTokenFromRequestBody(request).should.equal('foo'); + }); + }); + + describe('getAccessToken()', function () { + it('should throw an error if `accessToken` is missing', function () { + const model = Model.from({ + getAccessToken: function () {}, + }); + const handler = new AuthenticateHandler({ model: model }); + + return handler + .getAccessToken('foo') + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token is invalid'); + }); + }); + + it('should throw an error if `accessToken.user` is missing', function () { + const model = Model.from({ + getAccessToken: function () { + return {}; + }, + }); + const handler = new AuthenticateHandler({ model: model }); + + return handler + .getAccessToken('foo') + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `getAccessToken()` did not return a `user` object', + ); + }); + }); + + it('should return an access token', function () { + const accessToken = { user: {} }; + const model = Model.from({ + getAccessToken: function () { + return accessToken; + }, + }); + const handler = new AuthenticateHandler({ model: model }); + + return handler + .getAccessToken('foo') + .then(function (data) { + data.should.equal(accessToken); + }) + .catch(should.fail); + }); + + it('should support promises', function () { + const model = Model.from({ + getAccessToken: async function () { + return { user: {} }; + }, + }); + const handler = new AuthenticateHandler({ model: model }); + + handler.getAccessToken('foo').should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const model = Model.from({ + getAccessToken: function () { + return { user: {} }; + }, + }); + const handler = new AuthenticateHandler({ model: model }); + + handler.getAccessToken('foo').should.be.an.instanceOf(Promise); + }); + }); + + describe('validateAccessToken()', function () { + it('should throw an error if `accessToken` is expired', async function () { + const accessToken = { accessTokenExpiresAt: new Date(new Date() / 2) }; + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + + try { + await handler.validateAccessToken(accessToken); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidTokenError); + e.message.should.equal('Invalid token: access token has expired'); + } + }); + + it('should return an access token', function () { + const accessToken = { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + + handler.validateAccessToken(accessToken).should.equal(accessToken); + }); + }); + + describe('verifyScope()', function () { + it('should throw an error if `scope` is insufficient (deprecated)', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return false; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: 'foo', + }); + + return handler + .verifyScope(['foo']) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InsufficientScopeError); + e.message.should.equal( + 'Insufficient scope: authorized scope is insufficient', + ); + }); + }); + + it('should throw an error if `scope` is insufficient', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return false; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: ['foo'], + }); + + return handler + .verifyScope(['foo']) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InsufficientScopeError); + e.message.should.equal( + 'Insufficient scope: authorized scope is insufficient', + ); + }); + }); + + it('should support promises (deprecated)', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: 'foo', + }); + + handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); + }); + + it('should support promises', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: ['foo'], + }); + + handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises (deprecated)', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: 'foo', + }); + + handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () { + return true; + }, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: ['foo'], + }); + + handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); + }); + }); + + describe('updateResponse()', function () { + it('should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model: model, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ['foo', 'biz'] }); + + response.headers.should.not.have.property('x-accepted-oauth-scopes'); + }); + + it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified (deprecated)', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model: model, + scope: 'foo bar', + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ['foo', 'biz'] }); + + response.get('X-Accepted-OAuth-Scopes').should.equal('foo bar'); + }); + + it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: false, + model: model, + scope: ['foo', 'bar'], + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ['foo', 'biz'] }); + + response.get('X-Accepted-OAuth-Scopes').should.equal('foo bar'); + }); + + it('should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model: model, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ['foo', 'biz'] }); + + response.headers.should.not.have.property('x-oauth-scopes'); + }); + + it('should set the `X-Authorized-OAuth-Scopes` header (deprecated)', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model: model, + scope: 'foo bar', + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ['foo', 'biz'] }); + + response.get('X-OAuth-Scopes').should.equal('foo biz'); + }); + + it('should set the `X-Authorized-OAuth-Scopes` header', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: function () {}, + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: false, + addAuthorizedScopesHeader: true, + model: model, + scope: ['foo', 'bar'], + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateResponse(response, { scope: ['foo', 'biz'] }); + + response.get('X-OAuth-Scopes').should.equal('foo biz'); + }); + }); }); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index dcabe42a..8e5b67c3 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -1,2015 +1,2015 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AccessDeniedError = require("../../../lib/errors/access-denied-error"); -const AuthenticateHandler = require("../../../lib/handlers/authenticate-handler"); -const AuthorizeHandler = require("../../../lib/handlers/authorize-handler"); -const CodeResponseType = require("../../../lib/response-types/code-response-type"); -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const InvalidClientError = require("../../../lib/errors/invalid-client-error"); -const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); -const InvalidScopeError = require("../../../lib/errors/invalid-scope-error"); -const UnsupportedResponseTypeError = require("../../../lib/errors/unsupported-response-type-error"); -const Model = require("../../../lib/model"); -const Request = require("../../../lib/request"); -const Response = require("../../../lib/response"); -const ServerError = require("../../../lib/errors/server-error"); -const UnauthorizedClientError = require("../../../lib/errors/unauthorized-client-error"); -const should = require("chai").should(); -const url = require("url"); +const AccessDeniedError = require('../../../lib/errors/access-denied-error'); +const AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); +const AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); +const CodeResponseType = require('../../../lib/response-types/code-response-type'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const InvalidClientError = require('../../../lib/errors/invalid-client-error'); +const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +const InvalidScopeError = require('../../../lib/errors/invalid-scope-error'); +const UnsupportedResponseTypeError = require('../../../lib/errors/unsupported-response-type-error'); +const Model = require('../../../lib/model'); +const Request = require('../../../lib/request'); +const Response = require('../../../lib/response'); +const ServerError = require('../../../lib/errors/server-error'); +const UnauthorizedClientError = require('../../../lib/errors/unauthorized-client-error'); +const should = require('chai').should(); +const url = require('url'); const createModel = (model = {}) => { - return Model.from({ - getAccessToken: () => should.fail(), - getClient: () => should.fail(), - saveAuthorizationCode: () => should.fail(), - ...model, - }); + return Model.from({ + getAccessToken: () => should.fail(), + getClient: () => should.fail(), + saveAuthorizationCode: () => should.fail(), + ...model, + }); }; /** * Test `AuthorizeHandler` integration. */ -describe("AuthorizeHandler integration", function () { - describe("constructor()", function () { - it("should throw an error if `options.authorizationCodeLifetime` is missing", function () { - try { - new AuthorizeHandler(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Missing parameter: `authorizationCodeLifetime`", - ); - } - }); - - it("should throw an error if `options.model` is missing", function () { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120 }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `model`"); - } - }); - - it("should throw an error if the model does not implement `getClient()`", function () { - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `getClient()`", - ); - } - }); - - it("should throw an error if the model does not implement `saveAuthorizationCode()`", function () { - try { - new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model: { getClient: () => should.fail() }, - }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `saveAuthorizationCode()`", - ); - } - }); - - it("should throw an error if the model does not implement `getAccessToken()`", function () { - const model = Model.from({ - getClient: () => should.fail(), - saveAuthorizationCode: () => should.fail(), - }); - - try { - new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `getAccessToken()`", - ); - } - }); - - it("should set the `authorizationCodeLifetime`", function () { - const model = createModel(); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - handler.authorizationCodeLifetime.should.equal(120); - }); - - it("should throw if the custom `authenticateHandler` does not implement a `handle` method", function () { - const model = createModel(); - const authenticateHandler = {}; // misses handle() method - - try { - new AuthorizeHandler({ - authenticateHandler, - authorizationCodeLifetime: 120, - model, - }); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: authenticateHandler does not implement `handle()`", - ); - } - }); - - it("should set the default `authenticateHandler`, if no custom one is passed", function () { - const model = createModel(); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); - }); - - it("should set the custom `authenticateHandler`, if valid", function () { - const model = createModel(); - - class CustomAuthenticateHandler { - async handle() {} - } - - const authenticateHandler = new CustomAuthenticateHandler(); - const handler = new AuthorizeHandler({ - authenticateHandler, - authorizationCodeLifetime: 120, - model, - }); - handler.authenticateHandler.should.be.an.instanceOf( - CustomAuthenticateHandler, - ); - handler.authenticateHandler.should.not.be.an.instanceOf( - AuthenticateHandler, - ); - }); - - it("should set the `model`", function () { - const model = createModel(); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - handler.model.should.equal(model); - }); - }); - - describe("handle()", function () { - it("should throw an error if `request` is missing", async function () { - const model = createModel(); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - try { - await handler.handle(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: `request` must be an instance of Request", - ); - } - }); - - it("should throw an error if `response` is missing", async function () { - const model = createModel(); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.handle(request); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: `response` must be an instance of Response", - ); - } - }); - - it("should redirect to an error response if user denied access", async function () { - const client = { - id: "client-12345", - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const model = createModel({ - getAccessToken: async function (_token) { - _token.should.equal("foobarbazmootoken"); - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function (clientId, clientSecret) { - clientId.should.equal(client.id); - (clientSecret === null).should.equal(true); - return { ...client }; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: client.id, - response_type: "code", - }, - method: {}, - headers: { - Authorization: "Bearer foobarbazmootoken", - }, - query: { - state: "foobar", - allowed: "false", - }, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal( - "Access denied: user denied access to application", - ); - response - .get("location") - .should.equal( - "http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar", - ); - } - }); - - it("should redirect to an error response if a non-oauth error is thrown", async function () { - const model = createModel({ - getAccessToken: async function () { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function () { - return { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - }, - saveAuthorizationCode: async function () { - throw new CustomError("Unhandled exception"); - }, - }); - class CustomError extends Error {} - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - }, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: { - state: "foobar", - }, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(ServerError); // non-oauth-errors are converted to ServerError - e.message.should.equal("Unhandled exception"); - response - .get("location") - .should.equal( - "http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar", - ); - } - }); - - it("should redirect to an error response if an oauth error is thrown", async function () { - const model = createModel({ - getAccessToken: async function () { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function () { - return { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - }, - saveAuthorizationCode: async function () { - throw new AccessDeniedError("Cannot request this auth code"); - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - }, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: { - state: "foobar", - }, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal("Cannot request this auth code"); - response - .get("location") - .should.equal( - "http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar", - ); - } - }); - - it("should redirect to a successful response with `code` and `state` if successful", async function () { - const client = { - id: "client-12343434", - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const model = createModel({ - getAccessToken: async function (_token) { - _token.should.equal("foobarbaztokenmoo"); - return { - client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function (clientId, clientSecret) { - clientId.should.equal(client.id); - (clientSecret === null).should.equal(true); - return { ...client }; - }, - saveAuthorizationCode: async function () { - return { - authorizationCode: "fooobar-long-authzcode-?", - client, - }; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: client.id, - response_type: "code", - }, - headers: { - Authorization: "Bearer foobarbaztokenmoo", - }, - method: {}, - query: { - state: "foobarbazstatemoo", - }, - }); - const response = new Response({ body: {}, headers: {} }); - const data = await handler.handle(request, response); - data.authorizationCode.should.equal("fooobar-long-authzcode-?"); - data.client.should.deep.equal(client); - response.status.should.equal(302); - response - .get("location") - .should.equal( - "http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo", - ); - }); - - it("should redirect to an error response if `scope` is invalid", async function () { - const model = createModel({ - getAccessToken: async function () { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function () { - return { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - }, - saveAuthorizationCode: async function () { - return {}; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - }, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: { - scope: [], - state: "foobar", - }, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal("Invalid parameter: `scope`"); - response.status.should.equal(302); - response - .get("location") - .should.equal( - "http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar", - ); - } - }); - - it("should redirect to a successful response if `model.validateScope` is not defined", async function () { - const client = { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const model = Model.from({ - getAccessToken: function () { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: function () { - return client; - }, - saveAuthorizationCode: function () { - return { authorizationCode: "fooobar-long-authzcode-?", client }; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - }, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: { - scope: "read", - state: "foobarbazstatemoo", - }, - }); - const response = new Response({ body: {}, headers: {} }); - const data = await handler.handle(request, response); - data.should.deep.equal({ - authorizationCode: "fooobar-long-authzcode-?", - client: client, - }); - response.status.should.equal(302); - response - .get("location") - .should.equal( - "http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo", - ); - }); - - it("should redirect to an error response if `scope` is insufficient (validateScope)", async function () { - const client = { - id: 12345, - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const model = Model.from({ - getAccessToken: async function () { - return { - client: client, - user: { name: "foouser" }, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function () { - return client; - }, - saveAuthorizationCode: async function () { - return { authorizationCode: 12345, client }; - }, - validateScope: async function (_user, _client, _scope) { - _scope.should.eql(["read"]); - return false; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - }, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: { - scope: "read", - state: "foobar", - }, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal("Invalid scope: Requested scope is invalid"); - response.status.should.equal(302); - response - .get("location") - .should.equal( - "http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar", - ); - } - }); - - it("should redirect to an error response if `state` is missing", async function () { - const model = createModel({ - getAccessToken: async function () { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function () { - return { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - }, - saveAuthorizationCode: async function () { - throw new AccessDeniedError("Cannot request this auth code"); - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - }, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Missing parameter: `state`"); - response.status.should.equal(302); - response - .get("location") - .should.equal( - "http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60", - ); - } - }); - - it("should redirect to an error response if `response_type` is invalid", async function () { - const model = Model.from({ - getAccessToken: async function () { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function () { - return { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - }, - saveAuthorizationCode: () => should.fail(), // should fail before call - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "test", - }, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: { - state: "foobar", - }, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await handler.handle(request, response); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedResponseTypeError); - e.message.should.equal( - "Unsupported response type: `response_type` is not supported", - ); - response.status.should.equal(302); - response - .get("location") - .should.equal( - "http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar", - ); - } - }); - - it("should return the `code` if successful", async function () { - const client = { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const model = Model.from({ - getAccessToken: async function () { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function () { - return client; - }, - generateAuthorizationCode: async () => "some-code", - saveAuthorizationCode: async function (code) { - return { authorizationCode: code.authorizationCode, client: client }; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - }, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: { - state: "foobar", - }, - }); - const response = new Response({ body: {}, headers: {} }); - - const data = await handler.handle(request, response); - data.should.eql({ - authorizationCode: "some-code", - client: client, - }); - }); - - it("should return the `code` if successful (full model implementation)", async function () { - const user = { name: "fooUser" }; - const state = "fooobarstatebaz"; - const scope = ["read"]; - const client = { - id: "client-1322132131", - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const authorizationCode = "long-authz-code"; - const accessTokenDoc = { - accessToken: "some-access-token-code", - client, - user, - scope, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - const model = Model.from({ - getClient: async function (clientId, clientSecret) { - clientId.should.equal(client.id); - (clientSecret === null).should.equal(true); - return { ...client }; - }, - getAccessToken: async function (_token) { - _token.should.equal(accessTokenDoc.accessToken); - return { ...accessTokenDoc }; - }, - verifyScope: async function (_tokenDoc, _scope) { - _tokenDoc.should.equal(accessTokenDoc); - _scope.should.eql(accessTokenDoc.scope); - return true; - }, - validateScope: async function (_user, _client, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return _scope; - }, - generateAuthorizationCode: async function (_client, _user, _scope) { - _user.should.deep.equal(user); - _client.should.deep.equal(client); - _scope.should.eql(scope); - return authorizationCode; - }, - saveAuthorizationCode: async function (code, _client, _user) { - code.authorizationCode.should.equal(authorizationCode); - code.expiresAt.should.be.instanceOf(Date); - _user.should.deep.equal(user); - _client.should.deep.equal(client); - return { ...code, client, user }; - }, - }); - - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: client.id, - response_type: "code", - }, - headers: { - Authorization: `Bearer ${accessTokenDoc.accessToken}`, - }, - method: {}, - query: { state, scope: scope.join(" ") }, - }); - - const response = new Response({ body: {}, headers: {} }); - const data = await handler.handle(request, response); - data.scope.should.eql(scope); - data.client.should.deep.equal(client); - data.user.should.deep.equal(user); - data.expiresAt.should.be.instanceOf(Date); - data.redirectUri.should.equal(client.redirectUris[0]); - response.status.should.equal(302); - response - .get("location") - .should.equal( - "http://example.com/cb?code=long-authz-code&state=fooobarstatebaz", - ); - }); - - it("should support a custom `authenticateHandler`", async function () { - const user = { name: "user1" }; - const authenticateHandler = { - handle: async function () { - // all good - return { ...user }; - }, - }; - const client = { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const model = Model.from({ - getAccessToken: async function () { - return { - client: client, - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: async function () { - return client; - }, - generateAuthorizationCode: async () => "some-code", - saveAuthorizationCode: async function (code) { - return { authorizationCode: code.authorizationCode, client: client }; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - authenticateHandler, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - }, - headers: { - Authorization: "Bearer foo", - }, - method: {}, - query: { - state: "foobar", - }, - }); - const response = new Response({ body: {}, headers: {} }); - - const data = await handler.handle(request, response); - data.should.eql({ - authorizationCode: "some-code", - client: client, - }); - }); - }); - - describe("generateAuthorizationCode()", function () { - it("should return an auth code", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - return handler - .generateAuthorizationCode() - .then(function (data) { - data.should.be.a.sha256(); - }) - .catch(should.fail); - }); - - it("should support promises", function () { - const model = Model.from({ - generateAuthorizationCode: async function () { - return {}; - }, - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const model = Model.from({ - generateAuthorizationCode: function () { - return {}; - }, - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); - }); - }); - - describe("getAuthorizationCodeLifetime()", function () { - it("should return a date", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); - }); - }); - - describe("validateRedirectUri()", function () { - it("should support empty method", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - handler - .validateRedirectUri("http://example.com/a", { - redirectUris: ["http://example.com/a"], - }) - .should.be.an.instanceOf(Promise); - }); - - it("should support promises", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - validateRedirectUri: async function () { - return true; - }, - }); - - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - handler - .validateRedirectUri("http://example.com/a", {}) - .should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - validateRedirectUri: function () { - return true; - }, - }); - - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - handler - .validateRedirectUri("http://example.com/a", {}) - .should.be.an.instanceOf(Promise); - }); - }); - - describe("getClient()", function () { - it("should throw an error if `client_id` is missing", async function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { response_type: "code" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Missing parameter: `client_id`"); - } - }); - - it("should throw an error if `client_id` is invalid", async function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { client_id: "øå€£‰", response_type: "code" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `client_id`"); - } - }); - - it("should throw an error if `client.redirectUri` is invalid", async function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - redirect_uri: "foobar", - }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: `redirect_uri` is not a valid URI", - ); - } - }); - - it("should throw an error if `client` is missing", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { client_id: 12345, response_type: "code" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - "Invalid client: client credentials are invalid", - ); - }); - }); - - it("should throw an error if `client.grants` is missing", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () { - return {}; - }, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { client_id: 12345, response_type: "code" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal("Invalid client: missing client `grants`"); - }); - }); - - it("should throw an error if `client` is unauthorized", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () { - return { grants: [] }; - }, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { client_id: 12345, response_type: "code" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal( - "Unauthorized client: `grant_type` is invalid", - ); - }); - }); - - it("should throw an error if `client.redirectUri` is missing", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () { - return { grants: ["authorization_code"] }; - }, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { client_id: 12345, response_type: "code" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - "Invalid client: missing client `redirectUri`", - ); - }); - }); - - it("should throw an error if `client.redirectUri` is not equal to `redirectUri`", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () { - return { - grants: ["authorization_code"], - redirectUris: ["https://example.com"], - }; - }, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { - client_id: 12345, - response_type: "code", - redirect_uri: "https://foobar.com", - }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - "Invalid client: `redirect_uri` does not match client value", - ); - }); - }); - - it("should support promises", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: async function () { - return { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - }, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () { - return { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - }, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { client_id: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - describe("with `client_id` in the request query", function () { - it("should return a client", function () { - const client = { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () { - return client; - }, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { response_type: "code" }, - headers: {}, - method: {}, - query: { client_id: 12345 }, - }); - - return handler - .getClient(request) - .then(function (data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - }); - - describe("getScope()", function () { - it("should throw an error if `scope` is invalid", async function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { scope: "øå€£‰" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getScope(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidScopeError); - e.message.should.equal("Invalid parameter: `scope`"); - } - }); - - describe("with `scope` in the request body", function () { - it("should return the scope", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { scope: "foo" }, - headers: {}, - method: {}, - query: {}, - }); - - handler.getScope(request).should.eql(["foo"]); - }); - }); - - describe("with `scope` in the request query", function () { - it("should return the scope", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: { scope: "foo" }, - }); - - handler.getScope(request).should.eql(["foo"]); - }); - }); - }); - - describe("getState()", function () { - it("should throw an error if `allowEmptyState` is false and `state` is missing", async function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - allowEmptyState: false, - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getState(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Missing parameter: `state`"); - } - }); - - it("should allow missing `state` if `allowEmptyState` is valid", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - allowEmptyState: true, - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - const state = handler.getState(request); - should.equal(state, undefined); - }); - - it("should throw an error if `state` is invalid", async function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: { state: "øå€£‰" }, - }); - - try { - await handler.getState(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `state`"); - } - }); - - describe("with `state` in the request body", function () { - it("should return the state", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { state: "foobar" }, - headers: {}, - method: {}, - query: {}, - }); - - handler.getState(request).should.equal("foobar"); - }); - }); - - describe("with `state` in the request query", function () { - it("should return the state", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: { state: "foobar" }, - }); - - handler.getState(request).should.equal("foobar"); - }); - }); - }); - - describe("getUser()", function () { - it("should throw an error if `user` is missing", function () { - const authenticateHandler = { handle: function () {} }; - const model = Model.from({ - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authenticateHandler: authenticateHandler, - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - const response = new Response(); - - return handler - .getUser(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - "Server error: `handle()` did not return a `user` object", - ); - }); - }); - - it("should return a user", function () { - const user = {}; - const model = Model.from({ - getAccessToken: function () { - return { - user: user, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .getUser(request, response) - .then(function (data) { - data.should.equal(user); - }) - .catch(should.fail); - }); - }); - - describe("saveAuthorizationCode()", function () { - it("should return an auth code", function () { - const authorizationCode = {}; - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () { - return authorizationCode; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - return handler - .saveAuthorizationCode("foo", "bar", "biz", "baz") - .then(function (data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); - - it("should support promises when calling `model.saveAuthorizationCode()`", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: async function () { - return {}; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - handler - .saveAuthorizationCode("foo", "bar", "biz", "baz") - .should.be.an.instanceOf(Promise); - }); - - it("should support non-promises when calling `model.saveAuthorizationCode()`", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () { - return {}; - }, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - - handler - .saveAuthorizationCode("foo", "bar", "biz", "baz") - .should.be.an.instanceOf(Promise); - }); - }); - - describe("getResponseType()", function () { - it("should throw an error if `response_type` is missing", async function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getResponseType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Missing parameter: `response_type`"); - } - }); - - it("should throw an error if `response_type` is not `code`", async function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { response_type: "foobar" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getResponseType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedResponseTypeError); - e.message.should.equal( - "Unsupported response type: `response_type` is not supported", - ); - } - }); - - describe("with `response_type` in the request body", function () { - it("should return a response type", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { response_type: "code" }, - headers: {}, - method: {}, - query: {}, - }); - const ResponseType = handler.getResponseType(request); - - ResponseType.should.equal(CodeResponseType); - }); - }); - - describe("with `response_type` in the request query", function () { - it("should return a response type", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: { response_type: "code" }, - }); - const ResponseType = handler.getResponseType(request); - - ResponseType.should.equal(CodeResponseType); - }); - }); - }); - - describe("buildSuccessRedirectUri()", function () { - it("should return a redirect uri", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const responseType = new CodeResponseType(12345); - const redirectUri = handler.buildSuccessRedirectUri( - "http://example.com/cb", - responseType, - ); - - url.format(redirectUri).should.equal("http://example.com/cb?code=12345"); - }); - }); - - describe("buildErrorRedirectUri()", function () { - it("should set `error_description` if available", function () { - const error = new InvalidClientError("foo bar"); - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const redirectUri = handler.buildErrorRedirectUri( - "http://example.com/cb", - error, - ); - - url - .format(redirectUri) - .should.equal( - "http://example.com/cb?error=invalid_client&error_description=foo%20bar", - ); - }); - - it("should return a redirect uri", function () { - const error = new InvalidClientError(); - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const redirectUri = handler.buildErrorRedirectUri( - "http://example.com/cb", - error, - ); - - url - .format(redirectUri) - .should.equal( - "http://example.com/cb?error=invalid_client&error_description=Bad%20Request", - ); - }); - }); - - describe("updateResponse()", function () { - it("should set the `location` header", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const response = new Response({ body: {}, headers: {} }); - const uri = url.parse("http://example.com/cb"); - - handler.updateResponse(response, uri, "foobar"); - - response - .get("location") - .should.equal("http://example.com/cb?state=foobar"); - }); - }); - - describe("getCodeChallengeMethod()", function () { - it("should throw if the code challenge method is not supported", async function () { - const methods = ["plain", "foo", " ", "0", true, {}, []]; - - for (const method of methods) { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { code_challenge_method: method }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getCodeChallengeMethod(request); - should.fail(); - } catch (e) { - if (method === "plain") { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); - } else { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - `Invalid request: transform algorithm '${method}' not supported`, - ); - } - } - } - }); - - it("should get code challenge method", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { code_challenge_method: "S256" }, - headers: {}, - method: {}, - query: {}, - }); - - const codeChallengeMethod = handler.getCodeChallengeMethod(request); - codeChallengeMethod.should.equal("S256"); - }); - - it("should throw if the code challenge method is not supported", async function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { code_challenge_method: "foo" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getCodeChallengeMethod(request); - - should.fail(); - } catch (e) { - // defined in RFC 7636 - 4.4 - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: transform algorithm 'foo' not supported", - ); - } - }); - - it("should get default code challenge method S256 if missing (default)", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - const codeChallengeMethod = handler.getCodeChallengeMethod(request); - codeChallengeMethod.should.equal("S256"); - }); - - it("should get default code challenge method plain if missing and plain PKCE is enabled", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - enablePlainPKCE: true, - model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - const codeChallengeMethod = handler.getCodeChallengeMethod(request); - codeChallengeMethod.should.equal("plain"); - }); - }); - - describe("getCodeChallenge()", function () { - it("should get code challenge", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model, - }); - const request = new Request({ - body: { code_challenge: "challenge" }, - headers: {}, - method: {}, - query: {}, - }); - - const codeChallengeMethod = handler.getCodeChallenge(request); - codeChallengeMethod.should.equal("challenge"); - }); - }); +describe('AuthorizeHandler integration', function () { + describe('constructor()', function () { + it('should throw an error if `options.authorizationCodeLifetime` is missing', function () { + try { + new AuthorizeHandler(); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Missing parameter: `authorizationCodeLifetime`', + ); + } + }); + + it('should throw an error if `options.model` is missing', function () { + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120 }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if the model does not implement `getClient()`', function () { + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120, model: {} }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getClient()`', + ); + } + }); + + it('should throw an error if the model does not implement `saveAuthorizationCode()`', function () { + try { + new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: { getClient: () => should.fail() }, + }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `saveAuthorizationCode()`', + ); + } + }); + + it('should throw an error if the model does not implement `getAccessToken()`', function () { + const model = Model.from({ + getClient: () => should.fail(), + saveAuthorizationCode: () => should.fail(), + }); + + try { + new AuthorizeHandler({ authorizationCodeLifetime: 120, model }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getAccessToken()`', + ); + } + }); + + it('should set the `authorizationCodeLifetime`', function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.authorizationCodeLifetime.should.equal(120); + }); + + it('should throw if the custom `authenticateHandler` does not implement a `handle` method', function () { + const model = createModel(); + const authenticateHandler = {}; // misses handle() method + + try { + new AuthorizeHandler({ + authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: authenticateHandler does not implement `handle()`', + ); + } + }); + + it('should set the default `authenticateHandler`, if no custom one is passed', function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); + }); + + it('should set the custom `authenticateHandler`, if valid', function () { + const model = createModel(); + + class CustomAuthenticateHandler { + async handle() {} + } + + const authenticateHandler = new CustomAuthenticateHandler(); + const handler = new AuthorizeHandler({ + authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + handler.authenticateHandler.should.be.an.instanceOf( + CustomAuthenticateHandler, + ); + handler.authenticateHandler.should.not.be.an.instanceOf( + AuthenticateHandler, + ); + }); + + it('should set the `model`', function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + handler.model.should.equal(model); + }); + }); + + describe('handle()', function () { + it('should throw an error if `request` is missing', async function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + try { + await handler.handle(); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should throw an error if `response` is missing', async function () { + const model = createModel(); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handle(request); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `response` must be an instance of Response', + ); + } + }); + + it('should redirect to an error response if user denied access', async function () { + const client = { + id: 'client-12345', + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = createModel({ + getAccessToken: async function (_token) { + _token.should.equal('foobarbazmootoken'); + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function (clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: client.id, + response_type: 'code', + }, + method: {}, + headers: { + Authorization: 'Bearer foobarbazmootoken', + }, + query: { + state: 'foobar', + allowed: 'false', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal( + 'Access denied: user denied access to application', + ); + response + .get('location') + .should.equal( + 'http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar', + ); + } + }); + + it('should redirect to an error response if a non-oauth error is thrown', async function () { + const model = createModel({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: async function () { + throw new CustomError('Unhandled exception'); + }, + }); + class CustomError extends Error {} + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(ServerError); // non-oauth-errors are converted to ServerError + e.message.should.equal('Unhandled exception'); + response + .get('location') + .should.equal( + 'http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar', + ); + } + }); + + it('should redirect to an error response if an oauth error is thrown', async function () { + const model = createModel({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: async function () { + throw new AccessDeniedError('Cannot request this auth code'); + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(AccessDeniedError); + e.message.should.equal('Cannot request this auth code'); + response + .get('location') + .should.equal( + 'http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar', + ); + } + }); + + it('should redirect to a successful response with `code` and `state` if successful', async function () { + const client = { + id: 'client-12343434', + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = createModel({ + getAccessToken: async function (_token) { + _token.should.equal('foobarbaztokenmoo'); + return { + client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function (clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + }, + saveAuthorizationCode: async function () { + return { + authorizationCode: 'fooobar-long-authzcode-?', + client, + }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: client.id, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foobarbaztokenmoo', + }, + method: {}, + query: { + state: 'foobarbazstatemoo', + }, + }); + const response = new Response({ body: {}, headers: {} }); + const data = await handler.handle(request, response); + data.authorizationCode.should.equal('fooobar-long-authzcode-?'); + data.client.should.deep.equal(client); + response.status.should.equal(302); + response + .get('location') + .should.equal( + 'http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo', + ); + }); + + it('should redirect to an error response if `scope` is invalid', async function () { + const model = createModel({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: async function () { + return {}; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: { + scope: [], + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid parameter: `scope`'); + response.status.should.equal(302); + response + .get('location') + .should.equal( + 'http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar', + ); + } + }); + + it('should redirect to a successful response if `model.validateScope` is not defined', async function () { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = Model.from({ + getAccessToken: function () { + return { + client: client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: function () { + return client; + }, + saveAuthorizationCode: function () { + return { authorizationCode: 'fooobar-long-authzcode-?', client }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: { + scope: 'read', + state: 'foobarbazstatemoo', + }, + }); + const response = new Response({ body: {}, headers: {} }); + const data = await handler.handle(request, response); + data.should.deep.equal({ + authorizationCode: 'fooobar-long-authzcode-?', + client: client, + }); + response.status.should.equal(302); + response + .get('location') + .should.equal( + 'http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo', + ); + }); + + it('should redirect to an error response if `scope` is insufficient (validateScope)', async function () { + const client = { + id: 12345, + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = Model.from({ + getAccessToken: async function () { + return { + client: client, + user: { name: 'foouser' }, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return client; + }, + saveAuthorizationCode: async function () { + return { authorizationCode: 12345, client }; + }, + validateScope: async function (_user, _client, _scope) { + _scope.should.eql(['read']); + return false; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: { + scope: 'read', + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid scope: Requested scope is invalid'); + response.status.should.equal(302); + response + .get('location') + .should.equal( + 'http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar', + ); + } + }); + + it('should redirect to an error response if `state` is missing', async function () { + const model = createModel({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: async function () { + throw new AccessDeniedError('Cannot request this auth code'); + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `state`'); + response.status.should.equal(302); + response + .get('location') + .should.equal( + 'http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60', + ); + } + }); + + it('should redirect to an error response if `response_type` is invalid', async function () { + const model = Model.from({ + getAccessToken: async function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: () => should.fail(), // should fail before call + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'test', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await handler.handle(request, response); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedResponseTypeError); + e.message.should.equal( + 'Unsupported response type: `response_type` is not supported', + ); + response.status.should.equal(302); + response + .get('location') + .should.equal( + 'http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar', + ); + } + }); + + it('should return the `code` if successful', async function () { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = Model.from({ + getAccessToken: async function () { + return { + client: client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return client; + }, + generateAuthorizationCode: async () => 'some-code', + saveAuthorizationCode: async function (code) { + return { authorizationCode: code.authorizationCode, client: client }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + const data = await handler.handle(request, response); + data.should.eql({ + authorizationCode: 'some-code', + client: client, + }); + }); + + it('should return the `code` if successful (full model implementation)', async function () { + const user = { name: 'fooUser' }; + const state = 'fooobarstatebaz'; + const scope = ['read']; + const client = { + id: 'client-1322132131', + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const authorizationCode = 'long-authz-code'; + const accessTokenDoc = { + accessToken: 'some-access-token-code', + client, + user, + scope, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + const model = Model.from({ + getClient: async function (clientId, clientSecret) { + clientId.should.equal(client.id); + (clientSecret === null).should.equal(true); + return { ...client }; + }, + getAccessToken: async function (_token) { + _token.should.equal(accessTokenDoc.accessToken); + return { ...accessTokenDoc }; + }, + verifyScope: async function (_tokenDoc, _scope) { + _tokenDoc.should.equal(accessTokenDoc); + _scope.should.eql(accessTokenDoc.scope); + return true; + }, + validateScope: async function (_user, _client, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return _scope; + }, + generateAuthorizationCode: async function (_client, _user, _scope) { + _user.should.deep.equal(user); + _client.should.deep.equal(client); + _scope.should.eql(scope); + return authorizationCode; + }, + saveAuthorizationCode: async function (code, _client, _user) { + code.authorizationCode.should.equal(authorizationCode); + code.expiresAt.should.be.instanceOf(Date); + _user.should.deep.equal(user); + _client.should.deep.equal(client); + return { ...code, client, user }; + }, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: client.id, + response_type: 'code', + }, + headers: { + Authorization: `Bearer ${accessTokenDoc.accessToken}`, + }, + method: {}, + query: { state, scope: scope.join(' ') }, + }); + + const response = new Response({ body: {}, headers: {} }); + const data = await handler.handle(request, response); + data.scope.should.eql(scope); + data.client.should.deep.equal(client); + data.user.should.deep.equal(user); + data.expiresAt.should.be.instanceOf(Date); + data.redirectUri.should.equal(client.redirectUris[0]); + response.status.should.equal(302); + response + .get('location') + .should.equal( + 'http://example.com/cb?code=long-authz-code&state=fooobarstatebaz', + ); + }); + + it('should support a custom `authenticateHandler`', async function () { + const user = { name: 'user1' }; + const authenticateHandler = { + handle: async function () { + // all good + return { ...user }; + }, + }; + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = Model.from({ + getAccessToken: async function () { + return { + client: client, + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: async function () { + return client; + }, + generateAuthorizationCode: async () => 'some-code', + saveAuthorizationCode: async function (code) { + return { authorizationCode: code.authorizationCode, client: client }; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + authenticateHandler, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + }, + headers: { + Authorization: 'Bearer foo', + }, + method: {}, + query: { + state: 'foobar', + }, + }); + const response = new Response({ body: {}, headers: {} }); + + const data = await handler.handle(request, response); + data.should.eql({ + authorizationCode: 'some-code', + client: client, + }); + }); + }); + + describe('generateAuthorizationCode()', function () { + it('should return an auth code', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .generateAuthorizationCode() + .then(function (data) { + data.should.be.a.sha256(); + }) + .catch(should.fail); + }); + + it('should support promises', function () { + const model = Model.from({ + generateAuthorizationCode: async function () { + return {}; + }, + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const model = Model.from({ + generateAuthorizationCode: function () { + return {}; + }, + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); + }); + }); + + describe('getAuthorizationCodeLifetime()', function () { + it('should return a date', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); + }); + }); + + describe('validateRedirectUri()', function () { + it('should support empty method', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .validateRedirectUri('http://example.com/a', { + redirectUris: ['http://example.com/a'], + }) + .should.be.an.instanceOf(Promise); + }); + + it('should support promises', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + validateRedirectUri: async function () { + return true; + }, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .validateRedirectUri('http://example.com/a', {}) + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + validateRedirectUri: function () { + return true; + }, + }); + + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .validateRedirectUri('http://example.com/a', {}) + .should.be.an.instanceOf(Promise); + }); + }); + + describe('getClient()', function () { + it('should throw an error if `client_id` is missing', async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: 'code' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `client_id`'); + } + }); + + it('should throw an error if `client_id` is invalid', async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 'øå€£‰', response_type: 'code' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + } + }); + + it('should throw an error if `client.redirectUri` is invalid', async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + redirect_uri: 'foobar', + }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `redirect_uri` is not a valid URI', + ); + } + }); + + it('should throw an error if `client` is missing', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: client credentials are invalid', + ); + }); + }); + + it('should throw an error if `client.grants` is missing', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return {}; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: missing client `grants`'); + }); + }); + + it('should throw an error if `client` is unauthorized', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return { grants: [] }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(UnauthorizedClientError); + e.message.should.equal( + 'Unauthorized client: `grant_type` is invalid', + ); + }); + }); + + it('should throw an error if `client.redirectUri` is missing', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return { grants: ['authorization_code'] }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345, response_type: 'code' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: missing client `redirectUri`', + ); + }); + }); + + it('should throw an error if `client.redirectUri` is not equal to `redirectUri`', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return { + grants: ['authorization_code'], + redirectUris: ['https://example.com'], + }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { + client_id: 12345, + response_type: 'code', + redirect_uri: 'https://foobar.com', + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: `redirect_uri` does not match client value', + ); + }); + }); + + it('should support promises', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: async function () { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { client_id: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + + describe('with `client_id` in the request query', function () { + it('should return a client', function () { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () { + return client; + }, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: 'code' }, + headers: {}, + method: {}, + query: { client_id: 12345 }, + }); + + return handler + .getClient(request) + .then(function (data) { + data.should.equal(client); + }) + .catch(should.fail); + }); + }); + }); + + describe('getScope()', function () { + it('should throw an error if `scope` is invalid', async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { scope: 'øå€£‰' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getScope(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidScopeError); + e.message.should.equal('Invalid parameter: `scope`'); + } + }); + + describe('with `scope` in the request body', function () { + it('should return the scope', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { scope: 'foo' }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getScope(request).should.eql(['foo']); + }); + }); + + describe('with `scope` in the request query', function () { + it('should return the scope', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { scope: 'foo' }, + }); + + handler.getScope(request).should.eql(['foo']); + }); + }); + }); + + describe('getState()', function () { + it('should throw an error if `allowEmptyState` is false and `state` is missing', async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + allowEmptyState: false, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getState(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `state`'); + } + }); + + it('should allow missing `state` if `allowEmptyState` is valid', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + allowEmptyState: true, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + const state = handler.getState(request); + should.equal(state, undefined); + }); + + it('should throw an error if `state` is invalid', async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { state: 'øå€£‰' }, + }); + + try { + await handler.getState(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `state`'); + } + }); + + describe('with `state` in the request body', function () { + it('should return the state', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { state: 'foobar' }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getState(request).should.equal('foobar'); + }); + }); + + describe('with `state` in the request query', function () { + it('should return the state', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { state: 'foobar' }, + }); + + handler.getState(request).should.equal('foobar'); + }); + }); + }); + + describe('getUser()', function () { + it('should throw an error if `user` is missing', function () { + const authenticateHandler = { handle: function () {} }; + const model = Model.from({ + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authenticateHandler: authenticateHandler, + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + const response = new Response(); + + return handler + .getUser(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal( + 'Server error: `handle()` did not return a `user` object', + ); + }); + }); + + it('should return a user', function () { + const user = {}; + const model = Model.from({ + getAccessToken: function () { + return { + user: user, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getUser(request, response) + .then(function (data) { + data.should.equal(user); + }) + .catch(should.fail); + }); + }); + + describe('saveAuthorizationCode()', function () { + it('should return an auth code', function () { + const authorizationCode = {}; + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + return handler + .saveAuthorizationCode('foo', 'bar', 'biz', 'baz') + .then(function (data) { + data.should.equal(authorizationCode); + }) + .catch(should.fail); + }); + + it('should support promises when calling `model.saveAuthorizationCode()`', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: async function () { + return {}; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .saveAuthorizationCode('foo', 'bar', 'biz', 'baz') + .should.be.an.instanceOf(Promise); + }); + + it('should support non-promises when calling `model.saveAuthorizationCode()`', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () { + return {}; + }, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + + handler + .saveAuthorizationCode('foo', 'bar', 'biz', 'baz') + .should.be.an.instanceOf(Promise); + }); + }); + + describe('getResponseType()', function () { + it('should throw an error if `response_type` is missing', async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getResponseType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `response_type`'); + } + }); + + it('should throw an error if `response_type` is not `code`', async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: 'foobar' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getResponseType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedResponseTypeError); + e.message.should.equal( + 'Unsupported response type: `response_type` is not supported', + ); + } + }); + + describe('with `response_type` in the request body', function () { + it('should return a response type', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { response_type: 'code' }, + headers: {}, + method: {}, + query: {}, + }); + const ResponseType = handler.getResponseType(request); + + ResponseType.should.equal(CodeResponseType); + }); + }); + + describe('with `response_type` in the request query', function () { + it('should return a response type', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { response_type: 'code' }, + }); + const ResponseType = handler.getResponseType(request); + + ResponseType.should.equal(CodeResponseType); + }); + }); + }); + + describe('buildSuccessRedirectUri()', function () { + it('should return a redirect uri', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const responseType = new CodeResponseType(12345); + const redirectUri = handler.buildSuccessRedirectUri( + 'http://example.com/cb', + responseType, + ); + + url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); + }); + }); + + describe('buildErrorRedirectUri()', function () { + it('should set `error_description` if available', function () { + const error = new InvalidClientError('foo bar'); + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const redirectUri = handler.buildErrorRedirectUri( + 'http://example.com/cb', + error, + ); + + url + .format(redirectUri) + .should.equal( + 'http://example.com/cb?error=invalid_client&error_description=foo%20bar', + ); + }); + + it('should return a redirect uri', function () { + const error = new InvalidClientError(); + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const redirectUri = handler.buildErrorRedirectUri( + 'http://example.com/cb', + error, + ); + + url + .format(redirectUri) + .should.equal( + 'http://example.com/cb?error=invalid_client&error_description=Bad%20Request', + ); + }); + }); + + describe('updateResponse()', function () { + it('should set the `location` header', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const response = new Response({ body: {}, headers: {} }); + const uri = url.parse('http://example.com/cb'); + + handler.updateResponse(response, uri, 'foobar'); + + response + .get('location') + .should.equal('http://example.com/cb?state=foobar'); + }); + }); + + describe('getCodeChallengeMethod()', function () { + it('should throw if the code challenge method is not supported', async function () { + const methods = ['plain', 'foo', ' ', '0', true, {}, []]; + + for (const method of methods) { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { code_challenge_method: method }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getCodeChallengeMethod(request); + should.fail(); + } catch (e) { + if (method === 'plain') { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', + ); + } else { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + `Invalid request: transform algorithm '${method}' not supported`, + ); + } + } + } + }); + + it('should get code challenge method', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { code_challenge_method: 'S256' }, + headers: {}, + method: {}, + query: {}, + }); + + const codeChallengeMethod = handler.getCodeChallengeMethod(request); + codeChallengeMethod.should.equal('S256'); + }); + + it('should throw if the code challenge method is not supported', async function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { code_challenge_method: 'foo' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getCodeChallengeMethod(request); + + should.fail(); + } catch (e) { + // defined in RFC 7636 - 4.4 + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + "Invalid request: transform algorithm 'foo' not supported", + ); + } + }); + + it('should get default code challenge method S256 if missing (default)', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + const codeChallengeMethod = handler.getCodeChallengeMethod(request); + codeChallengeMethod.should.equal('S256'); + }); + + it('should get default code challenge method plain if missing and plain PKCE is enabled', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + enablePlainPKCE: true, + model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + const codeChallengeMethod = handler.getCodeChallengeMethod(request); + codeChallengeMethod.should.equal('plain'); + }); + }); + + describe('getCodeChallenge()', function () { + it('should get code challenge', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model, + }); + const request = new Request({ + body: { code_challenge: 'challenge' }, + headers: {}, + method: {}, + query: {}, + }); + + const codeChallengeMethod = handler.getCodeChallenge(request); + codeChallengeMethod.should.equal('challenge'); + }); + }); }); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 072f0ced..af1fe572 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -1,1880 +1,1880 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AccessDeniedError = require("../../../lib/errors/access-denied-error"); -const BearerTokenType = require("../../../lib/token-types/bearer-token-type"); -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const InvalidClientError = require("../../../lib/errors/invalid-client-error"); -const InvalidGrantError = require("../../../lib/errors/invalid-grant-error"); -const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); -const PasswordGrantType = require("../../../lib/grant-types/password-grant-type"); -const Model = require("../../../lib/model"); -const Request = require("../../../lib/request"); -const Response = require("../../../lib/response"); -const ServerError = require("../../../lib/errors/server-error"); -const TokenHandler = require("../../../lib/handlers/token-handler"); -const UnauthorizedClientError = require("../../../lib/errors/unauthorized-client-error"); -const UnsupportedGrantTypeError = require("../../../lib/errors/unsupported-grant-type-error"); -const should = require("chai").should(); -const util = require("util"); -const crypto = require("crypto"); -const stringUtil = require("../../../lib/utils/string-util"); +const AccessDeniedError = require('../../../lib/errors/access-denied-error'); +const BearerTokenType = require('../../../lib/token-types/bearer-token-type'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const InvalidClientError = require('../../../lib/errors/invalid-client-error'); +const InvalidGrantError = require('../../../lib/errors/invalid-grant-error'); +const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +const PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); +const Model = require('../../../lib/model'); +const Request = require('../../../lib/request'); +const Response = require('../../../lib/response'); +const ServerError = require('../../../lib/errors/server-error'); +const TokenHandler = require('../../../lib/handlers/token-handler'); +const UnauthorizedClientError = require('../../../lib/errors/unauthorized-client-error'); +const UnsupportedGrantTypeError = require('../../../lib/errors/unsupported-grant-type-error'); +const should = require('chai').should(); +const util = require('util'); +const crypto = require('crypto'); +const stringUtil = require('../../../lib/utils/string-util'); /** * Test `TokenHandler` integration. */ -describe("TokenHandler integration", function () { - describe("constructor()", function () { - it("should throw an error if `options.accessTokenLifetime` is missing", function () { - try { - new TokenHandler(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `accessTokenLifetime`"); - } - }); - - it("should throw an error if `options.model` is missing", function () { - try { - new TokenHandler({ accessTokenLifetime: 120 }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `model`"); - } - }); - - it("should throw an error if `options.refreshTokenLifetime` is missing", function () { - try { - new TokenHandler({ accessTokenLifetime: 120, model: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `refreshTokenLifetime`"); - } - }); - - it("should throw an error if the model does not implement `getClient()`", function () { - try { - new TokenHandler({ - accessTokenLifetime: 120, - model: {}, - refreshTokenLifetime: 120, - }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: model does not implement `getClient()`", - ); - } - }); - - it("should set the `accessTokenLifetime`", function () { - const accessTokenLifetime = {}; - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: accessTokenLifetime, - model: model, - refreshTokenLifetime: 120, - }); - - handler.accessTokenLifetime.should.equal(accessTokenLifetime); - }); - - it("should set the `alwaysIssueNewRefreshToken`", function () { - const alwaysIssueNewRefreshToken = true; - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 123, - model: model, - refreshTokenLifetime: 120, - alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, - }); - - handler.alwaysIssueNewRefreshToken.should.equal( - alwaysIssueNewRefreshToken, - ); - }); - - it("should set the `alwaysIssueNewRefreshToken` to false", function () { - const alwaysIssueNewRefreshToken = false; - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 123, - model: model, - refreshTokenLifetime: 120, - alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, - }); - - handler.alwaysIssueNewRefreshToken.should.equal( - alwaysIssueNewRefreshToken, - ); - }); - - it("should return the default `alwaysIssueNewRefreshToken` value", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 123, - model: model, - refreshTokenLifetime: 120, - }); - - handler.alwaysIssueNewRefreshToken.should.equal(true); - }); - - it("should set the `extendedGrantTypes`", function () { - const extendedGrantTypes = { foo: "bar" }; - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - extendedGrantTypes: extendedGrantTypes, - model: model, - refreshTokenLifetime: 120, - }); - handler.grantTypes.should.deep.include(extendedGrantTypes); - }); - - it("should set the `model`", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - - handler.model.should.equal(model); - }); - - it("should set the `refreshTokenLifetime`", function () { - const refreshTokenLifetime = {}; - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: refreshTokenLifetime, - }); - - handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); - }); - }); - - describe("handle()", function () { - it("should throw an error if `request` is missing", async function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - - try { - await handler.handle(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: `request` must be an instance of Request", - ); - } - }); - - it("should throw an error if `response` is missing", async function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.handle(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - "Invalid argument: `response` must be an instance of Response", - ); - } - }); - - it("should throw an error if the method is not `POST`", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: {}, - headers: {}, - method: "GET", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid request: method must be POST"); - }); - }); - - it("should throw an error if the media type is not `application/x-www-form-urlencoded`", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: {}, - headers: {}, - method: "POST", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: content must be application/x-www-form-urlencoded", - ); - }); - }); - - it("should throw the error if an oauth error is thrown", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: {}, - headers: { - "content-type": "application/x-www-form-urlencoded", - "transfer-encoding": "chunked", - }, - method: "POST", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - "Invalid client: cannot retrieve client credentials", - ); - }); - }); - - it("should throw a server error if a non-oauth error is thrown", function () { - const model = Model.from({ - getClient: function () { - throw new Error("Unhandled exception"); - }, - getUser: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: "secret", - grant_type: "password", - password: "bar", - username: "foo", - }, - headers: { - "content-type": "application/x-www-form-urlencoded", - "transfer-encoding": "chunked", - }, - method: "POST", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal("Unhandled exception"); - e.inner.should.be.an.instanceOf(Error); - }); - }); - - it("should update the response if an error is thrown", function () { - const model = Model.from({ - getClient: function () { - throw new Error("Unhandled exception"); - }, - getUser: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: "secret", - grant_type: "password", - password: "bar", - username: "foo", - }, - headers: { - "content-type": "application/x-www-form-urlencoded", - "transfer-encoding": "chunked", - }, - method: "POST", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(should.fail) - .catch(function () { - response.body.should.eql({ - error: "server_error", - error_description: "Unhandled exception", - }); - response.status.should.equal(503); - }); - }); - - it("should return a bearer token if successful", function () { - const token = { - accessToken: "foo", - client: {}, - refreshToken: "bar", - scope: ["foobar"], - user: {}, - }; - const model = Model.from({ - getClient: function () { - return { grants: ["password"] }; - }, - getUser: function () { - return {}; - }, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["baz"]; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: "secret", - username: "foo", - password: "bar", - grant_type: "password", - scope: "baz", - }, - headers: { - "content-type": "application/x-www-form-urlencoded", - "transfer-encoding": "chunked", - }, - method: "POST", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(function (data) { - data.should.eql(token); - }) - .catch(should.fail); - }); - - it("should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set", function () { - const token = { - accessToken: "foo", - client: {}, - refreshToken: "bar", - scope: ["baz"], - user: {}, - foo: "bar", - }; - const model = Model.from({ - getClient: function () { - return { grants: ["password"] }; - }, - getUser: function () { - return {}; - }, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["baz"]; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: "secret", - username: "foo", - password: "bar", - grant_type: "password", - scope: "baz", - }, - headers: { - "content-type": "application/x-www-form-urlencoded", - "transfer-encoding": "chunked", - }, - method: "POST", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(function () { - should.exist(response.body.access_token); - should.exist(response.body.refresh_token); - should.exist(response.body.token_type); - response.body.scope.should.eql("baz"); - should.not.exist(response.body.foo); - }) - .catch(should.fail); - }); - - it("should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set", function () { - const token = { - accessToken: "foo", - client: {}, - refreshToken: "bar", - scope: ["baz"], - user: {}, - foo: "bar", - }; - const model = Model.from({ - getClient: function () { - return { grants: ["password"] }; - }, - getUser: function () { - return {}; - }, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["baz"]; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - allowExtendedTokenAttributes: true, - }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: "secret", - username: "foo", - password: "bar", - grant_type: "password", - scope: "baz", - }, - headers: { - "content-type": "application/x-www-form-urlencoded", - "transfer-encoding": "chunked", - }, - method: "POST", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .handle(request, response) - .then(function () { - should.exist(response.body.access_token); - should.exist(response.body.refresh_token); - should.exist(response.body.token_type); - response.body.scope.should.eql("baz"); - should.exist(response.body.foo); - }) - .catch(should.fail); - }); - }); - - describe("getClient()", function () { - it("should throw an error if `clientId` is invalid", async function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: "øå€£‰", client_secret: "foo" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `client_id`"); - } - }); - - it("should throw an error if `clientSecret` is invalid", async function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: "foo", client_secret: "øå€£‰" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getClient(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `client_secret`"); - } - }); - - it("should throw an error if `client` is missing", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal("Invalid client: client is invalid"); - }); - }); - - it("should throw an error if `client.grants` is missing", function () { - const model = Model.from({ - getClient: function () { - return {}; - }, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal("Server error: missing client `grants`"); - }); - }); - - it("should throw an error if `client.grants` is invalid", function () { - const model = Model.from({ - getClient: function () { - return { grants: "foobar" }; - }, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(ServerError); - e.message.should.equal("Server error: `grants` must be an array"); - }); - }); - - it("should throw a 401 error if the client is invalid and the request contains an authorization header", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: {}, - headers: { - authorization: util.format( - "Basic %s", - Buffer.from("foo:bar").toString("base64"), - ), - }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - return handler - .getClient(request, response) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.code.should.equal(401); - e.message.should.equal("Invalid client: client is invalid"); - - response - .get("WWW-Authenticate") - .should.equal('Basic realm="Service"'); - }); - }); - - it("should return a client", function () { - const client = { id: 12345, grants: [] }; - const model = Model.from({ - getClient: function () { - return client; - }, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(function (data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - - describe("with `password` grant type and `requireClientAuthentication` is false", function () { - it("should return a client ", function () { - const client = { id: 12345, grants: [] }; - const model = Model.from({ - getClient: function () { - return client; - }, - saveToken: function () {}, - }); - - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - requireClientAuthentication: { - password: false, - }, - }); - const request = new Request({ - body: { client_id: "blah", grant_type: "password" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(function (data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - - describe("with `password` grant type and `requireClientAuthentication` is false and Authorization header", function () { - it("should return a client ", function () { - const client = { id: 12345, grants: [] }; - const model = Model.from({ - getClient: function () { - return client; - }, - saveToken: function () {}, - }); - - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - requireClientAuthentication: { - password: false, - }, - }); - const request = new Request({ - body: { grant_type: "password" }, - headers: { - authorization: util.format( - "Basic %s", - Buffer.from("blah:").toString("base64"), - ), - }, - method: {}, - query: {}, - }); - - return handler - .getClient(request) - .then(function (data) { - data.should.equal(client); - }) - .catch(should.fail); - }); - }); - - it("should support promises", function () { - const model = Model.from({ - getClient: async function () { - return { grants: [] }; - }, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret" }, - headers: {}, - method: {}, - query: {}, - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - - it("should support non-promises", function () { - const model = Model.from({ - getClient: function () { - return { grants: [] }; - }, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret" }, - headers: {}, - method: {}, - query: {}, - }); - - handler.getClient(request).should.be.an.instanceOf(Promise); - }); - }); - - describe("getClientCredentials()", function () { - it("should throw an error if `client_id` is missing", async function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_secret: "foo" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getClientCredentials(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - "Invalid client: cannot retrieve client credentials", - ); - } - }); - - it("should throw an error if `client_secret` is missing", async function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: "foo" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.getClientCredentials(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - "Invalid client: cannot retrieve client credentials", - ); - } - }); - - describe("with `client_id` and grant type is `password` and `requireClientAuthentication` is false", function () { - it("should return a client", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - requireClientAuthentication: { password: false }, - }); - const request = new Request({ - body: { client_id: "foo", grant_type: "password" }, - headers: {}, - method: {}, - query: {}, - }); - const credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: "foo" }); - }); - }); - - describe("with `client_id` and `client_secret` in the request header as basic auth", function () { - it("should return a client", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: {}, - headers: { - authorization: util.format( - "Basic %s", - Buffer.from("foo:bar").toString("base64"), - ), - }, - method: {}, - query: {}, - }); - const credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: "foo", clientSecret: "bar" }); - }); - }); - - describe("with `client_id` and `client_secret` in the request body", function () { - it("should return a client", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: "foo", client_secret: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - const credentials = handler.getClientCredentials(request); - - credentials.should.eql({ clientId: "foo", clientSecret: "bar" }); - }); - }); - }); - - describe("handleGrantType()", function () { - it("should throw an error if `grant_type` is missing", async function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Missing parameter: `grant_type`"); - } - }); - - it("should throw an error if `grant_type` is invalid", async function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { grant_type: "~foo~" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal("Invalid parameter: `grant_type`"); - } - }); - - it("should throw an error if `grant_type` is unsupported", async function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { grant_type: "foobar" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.handleGrantType(request); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnsupportedGrantTypeError); - e.message.should.equal( - "Unsupported grant type: `grant_type` is invalid", - ); - } - }); - - it("should throw an error if `grant_type` is unauthorized", async function () { - const client = { grants: ["client_credentials"] }; - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { grant_type: "password" }, - headers: {}, - method: {}, - query: {}, - }); - - try { - await handler.handleGrantType(request, client); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal("Unauthorized client: `grant_type` is invalid"); - } - }); - - it("should throw an invalid grant error if a non-oauth error is thrown", function () { - const client = { grants: ["password"] }; - const model = Model.from({ - getClient: function (clientId, password) { - return client; - }, - getUser: function (uid, pwd) {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { grant_type: "password", username: "foo", password: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .handleGrantType(request, client) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: user credentials are invalid"); - }); - }); - - describe("with grant_type `authorization_code`", function () { - it("should return a token", function () { - const client = { id: "foobar", grants: ["authorization_code"] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () { - return { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date() * 2), - user: {}, - }; - }, - getClient: function () {}, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["foo"]; - }, - revokeAuthorizationCode: function () { - return { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date() / 2), - user: {}, - }; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - code: 12345, - grant_type: "authorization_code", - }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .handleGrantType(request, client) - .then(function (data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe("with PKCE", function () { - it("should return a token when code verifier is valid using S256 code challenge method", async function () { - const methods = ["S256", undefined]; - - for (const method of methods) { - const codeVerifier = stringUtil.base64URLEncode( - crypto.randomBytes(32), - ); - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: method, - codeChallenge: stringUtil.base64URLEncode( - crypto.createHash("sha256").update(codeVerifier).digest(), - ), - }; - const client = { id: "foobar", grants: ["authorization_code"] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - getClient: function () {}, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["foo"]; - }, - revokeAuthorizationCode: function () { - return authorizationCode; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - code: 12345, - grant_type: "authorization_code", - code_verifier: codeVerifier, - }, - headers: {}, - method: {}, - query: {}, - }); - - const data = await handler.handleGrantType(request, client); - data.should.equal(token); - } - }); - - it("should return a token when code verifier is valid using plain code challenge method", async function () { - const methods = ["plain", undefined]; - - for (const method of methods) { - const codeVerifier = stringUtil.base64URLEncode( - crypto.randomBytes(32), - ); - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: method, - codeChallenge: codeVerifier, - }; - const client = { id: "foobar", grants: ["authorization_code"] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - getClient: function () {}, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["foo"]; - }, - revokeAuthorizationCode: function () { - return authorizationCode; - }, - }); - const handler = new TokenHandler({ - enablePlainPKCE: true, - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - code: 12345, - grant_type: "authorization_code", - code_verifier: codeVerifier, - }, - headers: {}, - method: {}, - query: {}, - }); - - const data = await handler.handleGrantType(request, client); - data.should.equal(token); - } - }); - - it("should throw an invalid grant error when code verifier is invalid", function () { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: "S256", - codeChallenge: stringUtil.base64URLEncode( - crypto.createHash("sha256").update(codeVerifier).digest(), - ), - }; - const client = { id: "foobar", grants: ["authorization_code"] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - getClient: function () {}, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["foo"]; - }, - revokeAuthorizationCode: function () { - return authorizationCode; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - code: 12345, - grant_type: "authorization_code", - code_verifier: "123123123123123123123123123123123123123123123", - }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .handleGrantType(request, client) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: code verifier is invalid"); - }); - }); - - it("should throw an invalid grant error when code verifier is missing", function () { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: "S256", - codeChallenge: stringUtil.base64URLEncode( - crypto.createHash("sha256").update(codeVerifier).digest(), - ), - }; - const client = { id: "foobar", grants: ["authorization_code"] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - getClient: function () {}, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["foo"]; - }, - revokeAuthorizationCode: function () { - return authorizationCode; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - code: 12345, - grant_type: "authorization_code", - }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .handleGrantType(request, client) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Missing parameter: `code_verifier`"); - }); - }); - - it("should throw an invalid grant error when code verifier is present but code challenge is missing", function () { - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - }; - const client = { id: "foobar", grants: ["authorization_code"] }; - const token = {}; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - getClient: function () {}, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["foo"]; - }, - revokeAuthorizationCode: function () { - return authorizationCode; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - code: 12345, - grant_type: "authorization_code", - code_verifier: "123123123123123123123123123123123123123123123", - }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .handleGrantType(request, client) - .then(should.fail) - .catch(function (e) { - e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal("Invalid grant: code verifier is invalid"); - }); - }); - }); - - describe("with grant_type `client_credentials`", function () { - it("should return a token", function () { - const client = { grants: ["client_credentials"] }; - const token = {}; - const model = Model.from({ - getClient: function () {}, - getUserFromClient: function () { - return {}; - }, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["foo"]; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - grant_type: "client_credentials", - scope: "foo", - }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .handleGrantType(request, client) - .then(function (data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe("with grant_type `password`", function () { - it("should return a token", function () { - const client = { grants: ["password"] }; - const token = {}; - const model = Model.from({ - getClient: function () {}, - getUser: function () { - return {}; - }, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["baz"]; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - client_id: 12345, - client_secret: "secret", - grant_type: "password", - password: "bar", - username: "foo", - scope: "baz", - }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .handleGrantType(request, client) - .then(function (data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe("with grant_type `refresh_token`", function () { - it("should return a token", function () { - const client = { grants: ["refresh_token"] }; - const token = { accessToken: "foo", client: {}, user: {} }; - const model = Model.from({ - getClient: function () {}, - getRefreshToken: function () { - return { - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() * 2), - user: {}, - }; - }, - saveToken: function () { - return token; - }, - revokeToken: function () { - return { - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { - grant_type: "refresh_token", - refresh_token: 12345, - }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .handleGrantType(request, client) - .then(function (data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - - describe("with custom grant_type", function () { - it("should return a token", function () { - const client = { - grants: ["urn:ietf:params:oauth:grant-type:saml2-bearer"], - }; - const token = {}; - const model = Model.from({ - getClient: function () {}, - getUser: function () { - return {}; - }, - saveToken: function () { - return token; - }, - validateScope: function () { - return ["foo"]; - }, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - extendedGrantTypes: { - "urn:ietf:params:oauth:grant-type:saml2-bearer": PasswordGrantType, - }, - }); - const request = new Request({ - body: { - grant_type: "urn:ietf:params:oauth:grant-type:saml2-bearer", - username: "foo", - password: "bar", - }, - headers: {}, - method: {}, - query: {}, - }); - - return handler - .handleGrantType(request, client) - .then(function (data) { - data.should.equal(token); - }) - .catch(should.fail); - }); - }); - }); - - describe("getAccessTokenLifetime()", function () { - it("should return the client access token lifetime", function () { - const client = { accessTokenLifetime: 60 }; - const model = Model.from({ - getClient: function () { - return client; - }, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - - handler.getAccessTokenLifetime(client).should.equal(60); - }); - - it("should return the default access token lifetime", function () { - const client = {}; - const model = Model.from({ - getClient: function () { - return client; - }, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - - handler.getAccessTokenLifetime(client).should.equal(120); - }); - }); - - describe("getRefreshTokenLifetime()", function () { - it("should return the client access token lifetime", function () { - const client = { refreshTokenLifetime: 60 }; - const model = Model.from({ - getClient: function () { - return client; - }, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - - handler.getRefreshTokenLifetime(client).should.equal(60); - }); - - it("should return the default access token lifetime", function () { - const client = {}; - const model = Model.from({ - getClient: function () { - return client; - }, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - - handler.getRefreshTokenLifetime(client).should.equal(120); - }); - }); - - describe("getTokenType()", function () { - it("should return a token type", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const tokenType = handler.getTokenType({ - accessToken: "foo", - refreshToken: "bar", - scope: ["foobar"], - }); - tokenType.should.deep.include({ - accessToken: "foo", - accessTokenLifetime: undefined, - refreshToken: "bar", - scope: ["foobar"], - }); - }); - }); - - describe("updateSuccessResponse()", function () { - it("should set the `body`", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const tokenType = new BearerTokenType("foo", "bar", "biz"); - const response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.body.should.eql({ - access_token: "foo", - expires_in: "bar", - refresh_token: "biz", - token_type: "Bearer", - }); - }); - - it("should set the `Cache-Control` header", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const tokenType = new BearerTokenType("foo", "bar", "biz"); - const response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.get("Cache-Control").should.equal("no-store"); - }); - - it("should set the `Pragma` header", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const tokenType = new BearerTokenType("foo", "bar", "biz"); - const response = new Response({ body: {}, headers: {} }); - - handler.updateSuccessResponse(response, tokenType); - - response.get("Pragma").should.equal("no-cache"); - }); - }); - - describe("updateErrorResponse()", function () { - it("should set the `body`", function () { - const error = new AccessDeniedError("Cannot request a token"); - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateErrorResponse(response, error); - - response.body.error.should.equal("access_denied"); - response.body.error_description.should.equal("Cannot request a token"); - }); - - it("should set the `status`", function () { - const error = new AccessDeniedError("Cannot request a token"); - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const response = new Response({ body: {}, headers: {} }); - - handler.updateErrorResponse(response, error); - - response.status.should.equal(400); - }); - }); +describe('TokenHandler integration', function () { + describe('constructor()', function () { + it('should throw an error if `options.accessTokenLifetime` is missing', function () { + try { + new TokenHandler(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessTokenLifetime`'); + } + }); + + it('should throw an error if `options.model` is missing', function () { + try { + new TokenHandler({ accessTokenLifetime: 120 }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + + it('should throw an error if `options.refreshTokenLifetime` is missing', function () { + try { + new TokenHandler({ accessTokenLifetime: 120, model: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `refreshTokenLifetime`'); + } + }); + + it('should throw an error if the model does not implement `getClient()`', function () { + try { + new TokenHandler({ + accessTokenLifetime: 120, + model: {}, + refreshTokenLifetime: 120, + }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: model does not implement `getClient()`', + ); + } + }); + + it('should set the `accessTokenLifetime`', function () { + const accessTokenLifetime = {}; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: accessTokenLifetime, + model: model, + refreshTokenLifetime: 120, + }); + + handler.accessTokenLifetime.should.equal(accessTokenLifetime); + }); + + it('should set the `alwaysIssueNewRefreshToken`', function () { + const alwaysIssueNewRefreshToken = true; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 120, + alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, + }); + + handler.alwaysIssueNewRefreshToken.should.equal( + alwaysIssueNewRefreshToken, + ); + }); + + it('should set the `alwaysIssueNewRefreshToken` to false', function () { + const alwaysIssueNewRefreshToken = false; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 120, + alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, + }); + + handler.alwaysIssueNewRefreshToken.should.equal( + alwaysIssueNewRefreshToken, + ); + }); + + it('should return the default `alwaysIssueNewRefreshToken` value', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 123, + model: model, + refreshTokenLifetime: 120, + }); + + handler.alwaysIssueNewRefreshToken.should.equal(true); + }); + + it('should set the `extendedGrantTypes`', function () { + const extendedGrantTypes = { foo: 'bar' }; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + extendedGrantTypes: extendedGrantTypes, + model: model, + refreshTokenLifetime: 120, + }); + handler.grantTypes.should.deep.include(extendedGrantTypes); + }); + + it('should set the `model`', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.model.should.equal(model); + }); + + it('should set the `refreshTokenLifetime`', function () { + const refreshTokenLifetime = {}; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: refreshTokenLifetime, + }); + + handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); + }); + }); + + describe('handle()', function () { + it('should throw an error if `request` is missing', async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + try { + await handler.handle(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `request` must be an instance of Request', + ); + } + }); + + it('should throw an error if `response` is missing', async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handle(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal( + 'Invalid argument: `response` must be an instance of Response', + ); + } + }); + + it('should throw an error if the method is not `POST`', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'GET', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid request: method must be POST'); + }); + }); + + it('should throw an error if the media type is not `application/x-www-form-urlencoded`', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: content must be application/x-www-form-urlencoded', + ); + }); + }); + + it('should throw the error if an oauth error is thrown', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + }); + }); + + it('should throw a server error if a non-oauth error is thrown', function () { + const model = Model.from({ + getClient: function () { + throw new Error('Unhandled exception'); + }, + getUser: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Unhandled exception'); + e.inner.should.be.an.instanceOf(Error); + }); + }); + + it('should update the response if an error is thrown', function () { + const model = Model.from({ + getClient: function () { + throw new Error('Unhandled exception'); + }, + getUser: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(should.fail) + .catch(function () { + response.body.should.eql({ + error: 'server_error', + error_description: 'Unhandled exception', + }); + response.status.should.equal(503); + }); + }); + + it('should return a bearer token if successful', function () { + const token = { + accessToken: 'foo', + client: {}, + refreshToken: 'bar', + scope: ['foobar'], + user: {}, + }; + const model = Model.from({ + getClient: function () { + return { grants: ['password'] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['baz']; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + username: 'foo', + password: 'bar', + grant_type: 'password', + scope: 'baz', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function (data) { + data.should.eql(token); + }) + .catch(should.fail); + }); + + it('should not return custom attributes in a bearer token if the allowExtendedTokenAttributes is not set', function () { + const token = { + accessToken: 'foo', + client: {}, + refreshToken: 'bar', + scope: ['baz'], + user: {}, + foo: 'bar', + }; + const model = Model.from({ + getClient: function () { + return { grants: ['password'] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['baz']; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + username: 'foo', + password: 'bar', + grant_type: 'password', + scope: 'baz', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function () { + should.exist(response.body.access_token); + should.exist(response.body.refresh_token); + should.exist(response.body.token_type); + response.body.scope.should.eql('baz'); + should.not.exist(response.body.foo); + }) + .catch(should.fail); + }); + + it('should return custom attributes in a bearer token if the allowExtendedTokenAttributes is set', function () { + const token = { + accessToken: 'foo', + client: {}, + refreshToken: 'bar', + scope: ['baz'], + user: {}, + foo: 'bar', + }; + const model = Model.from({ + getClient: function () { + return { grants: ['password'] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['baz']; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + allowExtendedTokenAttributes: true, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + username: 'foo', + password: 'bar', + grant_type: 'password', + scope: 'baz', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .handle(request, response) + .then(function () { + should.exist(response.body.access_token); + should.exist(response.body.refresh_token); + should.exist(response.body.token_type); + response.body.scope.should.eql('baz'); + should.exist(response.body.foo); + }) + .catch(should.fail); + }); + }); + + describe('getClient()', function () { + it('should throw an error if `clientId` is invalid', async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'øå€£‰', client_secret: 'foo' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_id`'); + } + }); + + it('should throw an error if `clientSecret` is invalid', async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'øå€£‰' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClient(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `client_secret`'); + } + }); + + it('should throw an error if `client` is missing', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal('Invalid client: client is invalid'); + }); + }); + + it('should throw an error if `client.grants` is missing', function () { + const model = Model.from({ + getClient: function () { + return {}; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: missing client `grants`'); + }); + }); + + it('should throw an error if `client.grants` is invalid', function () { + const model = Model.from({ + getClient: function () { + return { grants: 'foobar' }; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(ServerError); + e.message.should.equal('Server error: `grants` must be an array'); + }); + }); + + it('should throw a 401 error if the client is invalid and the request contains an authorization header', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + return handler + .getClient(request, response) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.code.should.equal(401); + e.message.should.equal('Invalid client: client is invalid'); + + response + .get('WWW-Authenticate') + .should.equal('Basic realm="Service"'); + }); + }); + + it('should return a client', function () { + const client = { id: 12345, grants: [] }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(function (data) { + data.should.equal(client); + }) + .catch(should.fail); + }); + + describe('with `password` grant type and `requireClientAuthentication` is false', function () { + it('should return a client ', function () { + const client = { id: 12345, grants: [] }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + requireClientAuthentication: { + password: false, + }, + }); + const request = new Request({ + body: { client_id: 'blah', grant_type: 'password' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(function (data) { + data.should.equal(client); + }) + .catch(should.fail); + }); + }); + + describe('with `password` grant type and `requireClientAuthentication` is false and Authorization header', function () { + it('should return a client ', function () { + const client = { id: 12345, grants: [] }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + requireClientAuthentication: { + password: false, + }, + }); + const request = new Request({ + body: { grant_type: 'password' }, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('blah:').toString('base64'), + ), + }, + method: {}, + query: {}, + }); + + return handler + .getClient(request) + .then(function (data) { + data.should.equal(client); + }) + .catch(should.fail); + }); + }); + + it('should support promises', function () { + const model = Model.from({ + getClient: async function () { + return { grants: [] }; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + + it('should support non-promises', function () { + const model = Model.from({ + getClient: function () { + return { grants: [] }; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: {}, + query: {}, + }); + + handler.getClient(request).should.be.an.instanceOf(Promise); + }); + }); + + describe('getClientCredentials()', function () { + it('should throw an error if `client_id` is missing', async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_secret: 'foo' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClientCredentials(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + it('should throw an error if `client_secret` is missing', async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'foo' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.getClientCredentials(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidClientError); + e.message.should.equal( + 'Invalid client: cannot retrieve client credentials', + ); + } + }); + + describe('with `client_id` and grant type is `password` and `requireClientAuthentication` is false', function () { + it('should return a client', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + requireClientAuthentication: { password: false }, + }); + const request = new Request({ + body: { client_id: 'foo', grant_type: 'password' }, + headers: {}, + method: {}, + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo' }); + }); + }); + + describe('with `client_id` and `client_secret` in the request header as basic auth', function () { + it('should return a client', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: { + authorization: util.format( + 'Basic %s', + Buffer.from('foo:bar').toString('base64'), + ), + }, + method: {}, + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + + describe('with `client_id` and `client_secret` in the request body', function () { + it('should return a client', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 'foo', client_secret: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + const credentials = handler.getClientCredentials(request); + + credentials.should.eql({ clientId: 'foo', clientSecret: 'bar' }); + }); + }); + }); + + describe('handleGrantType()', function () { + it('should throw an error if `grant_type` is missing', async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handleGrantType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Missing parameter: `grant_type`'); + } + }); + + it('should throw an error if `grant_type` is invalid', async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: '~foo~' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handleGrantType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal('Invalid parameter: `grant_type`'); + } + }); + + it('should throw an error if `grant_type` is unsupported', async function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: 'foobar' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handleGrantType(request); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnsupportedGrantTypeError); + e.message.should.equal( + 'Unsupported grant type: `grant_type` is invalid', + ); + } + }); + + it('should throw an error if `grant_type` is unauthorized', async function () { + const client = { grants: ['client_credentials'] }; + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: 'password' }, + headers: {}, + method: {}, + query: {}, + }); + + try { + await handler.handleGrantType(request, client); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(UnauthorizedClientError); + e.message.should.equal('Unauthorized client: `grant_type` is invalid'); + } + }); + + it('should throw an invalid grant error if a non-oauth error is thrown', function () { + const client = { grants: ['password'] }; + const model = Model.from({ + getClient: function (clientId, password) { + return client; + }, + getUser: function (uid, pwd) {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { grant_type: 'password', username: 'foo', password: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: user credentials are invalid'); + }); + }); + + describe('with grant_type `authorization_code`', function () { + it('should return a token', function () { + const client = { id: 'foobar', grants: ['authorization_code'] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['foo']; + }, + revokeAuthorizationCode: function () { + return { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: 'authorization_code', + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe('with PKCE', function () { + it('should return a token when code verifier is valid using S256 code challenge method', async function () { + const methods = ['S256', undefined]; + + for (const method of methods) { + const codeVerifier = stringUtil.base64URLEncode( + crypto.randomBytes(32), + ); + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: method, + codeChallenge: stringUtil.base64URLEncode( + crypto.createHash('sha256').update(codeVerifier).digest(), + ), + }; + const client = { id: 'foobar', grants: ['authorization_code'] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['foo']; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: 'authorization_code', + code_verifier: codeVerifier, + }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await handler.handleGrantType(request, client); + data.should.equal(token); + } + }); + + it('should return a token when code verifier is valid using plain code challenge method', async function () { + const methods = ['plain', undefined]; + + for (const method of methods) { + const codeVerifier = stringUtil.base64URLEncode( + crypto.randomBytes(32), + ); + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: method, + codeChallenge: codeVerifier, + }; + const client = { id: 'foobar', grants: ['authorization_code'] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['foo']; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + enablePlainPKCE: true, + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: 'authorization_code', + code_verifier: codeVerifier, + }, + headers: {}, + method: {}, + query: {}, + }); + + const data = await handler.handleGrantType(request, client); + data.should.equal(token); + } + }); + + it('should throw an invalid grant error when code verifier is invalid', function () { + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: 'S256', + codeChallenge: stringUtil.base64URLEncode( + crypto.createHash('sha256').update(codeVerifier).digest(), + ), + }; + const client = { id: 'foobar', grants: ['authorization_code'] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['foo']; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: 'authorization_code', + code_verifier: '123123123123123123123123123123123123123123123', + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: code verifier is invalid'); + }); + }); + + it('should throw an invalid grant error when code verifier is missing', function () { + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: 'S256', + codeChallenge: stringUtil.base64URLEncode( + crypto.createHash('sha256').update(codeVerifier).digest(), + ), + }; + const client = { id: 'foobar', grants: ['authorization_code'] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['foo']; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: 'authorization_code', + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Missing parameter: `code_verifier`'); + }); + }); + + it('should throw an invalid grant error when code verifier is present but code challenge is missing', function () { + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + }; + const client = { id: 'foobar', grants: ['authorization_code'] }; + const token = {}; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + getClient: function () {}, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['foo']; + }, + revokeAuthorizationCode: function () { + return authorizationCode; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + code: 12345, + grant_type: 'authorization_code', + code_verifier: '123123123123123123123123123123123123123123123', + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(should.fail) + .catch(function (e) { + e.should.be.an.instanceOf(InvalidGrantError); + e.message.should.equal('Invalid grant: code verifier is invalid'); + }); + }); + }); + + describe('with grant_type `client_credentials`', function () { + it('should return a token', function () { + const client = { grants: ['client_credentials'] }; + const token = {}; + const model = Model.from({ + getClient: function () {}, + getUserFromClient: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['foo']; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + grant_type: 'client_credentials', + scope: 'foo', + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe('with grant_type `password`', function () { + it('should return a token', function () { + const client = { grants: ['password'] }; + const token = {}; + const model = Model.from({ + getClient: function () {}, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['baz']; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + client_id: 12345, + client_secret: 'secret', + grant_type: 'password', + password: 'bar', + username: 'foo', + scope: 'baz', + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe('with grant_type `refresh_token`', function () { + it('should return a token', function () { + const client = { grants: ['refresh_token'] }; + const token = { accessToken: 'foo', client: {}, user: {} }; + const model = Model.from({ + getClient: function () {}, + getRefreshToken: function () { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() * 2), + user: {}, + }; + }, + saveToken: function () { + return token; + }, + revokeToken: function () { + return { + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { + grant_type: 'refresh_token', + refresh_token: 12345, + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + + describe('with custom grant_type', function () { + it('should return a token', function () { + const client = { + grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'], + }; + const token = {}; + const model = Model.from({ + getClient: function () {}, + getUser: function () { + return {}; + }, + saveToken: function () { + return token; + }, + validateScope: function () { + return ['foo']; + }, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + extendedGrantTypes: { + 'urn:ietf:params:oauth:grant-type:saml2-bearer': PasswordGrantType, + }, + }); + const request = new Request({ + body: { + grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', + username: 'foo', + password: 'bar', + }, + headers: {}, + method: {}, + query: {}, + }); + + return handler + .handleGrantType(request, client) + .then(function (data) { + data.should.equal(token); + }) + .catch(should.fail); + }); + }); + }); + + describe('getAccessTokenLifetime()', function () { + it('should return the client access token lifetime', function () { + const client = { accessTokenLifetime: 60 }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.getAccessTokenLifetime(client).should.equal(60); + }); + + it('should return the default access token lifetime', function () { + const client = {}; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.getAccessTokenLifetime(client).should.equal(120); + }); + }); + + describe('getRefreshTokenLifetime()', function () { + it('should return the client access token lifetime', function () { + const client = { refreshTokenLifetime: 60 }; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.getRefreshTokenLifetime(client).should.equal(60); + }); + + it('should return the default access token lifetime', function () { + const client = {}; + const model = Model.from({ + getClient: function () { + return client; + }, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + + handler.getRefreshTokenLifetime(client).should.equal(120); + }); + }); + + describe('getTokenType()', function () { + it('should return a token type', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const tokenType = handler.getTokenType({ + accessToken: 'foo', + refreshToken: 'bar', + scope: ['foobar'], + }); + tokenType.should.deep.include({ + accessToken: 'foo', + accessTokenLifetime: undefined, + refreshToken: 'bar', + scope: ['foobar'], + }); + }); + }); + + describe('updateSuccessResponse()', function () { + it('should set the `body`', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType('foo', 'bar', 'biz'); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.body.should.eql({ + access_token: 'foo', + expires_in: 'bar', + refresh_token: 'biz', + token_type: 'Bearer', + }); + }); + + it('should set the `Cache-Control` header', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType('foo', 'bar', 'biz'); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.get('Cache-Control').should.equal('no-store'); + }); + + it('should set the `Pragma` header', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const tokenType = new BearerTokenType('foo', 'bar', 'biz'); + const response = new Response({ body: {}, headers: {} }); + + handler.updateSuccessResponse(response, tokenType); + + response.get('Pragma').should.equal('no-cache'); + }); + }); + + describe('updateErrorResponse()', function () { + it('should set the `body`', function () { + const error = new AccessDeniedError('Cannot request a token'); + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.body.error.should.equal('access_denied'); + response.body.error_description.should.equal('Cannot request a token'); + }); + + it('should set the `status`', function () { + const error = new AccessDeniedError('Cannot request a token'); + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const response = new Response({ body: {}, headers: {} }); + + handler.updateErrorResponse(response, error); + + response.status.should.equal(400); + }); + }); }); diff --git a/test/integration/request_test.js b/test/integration/request_test.js index 2b6bd9ba..7766603f 100644 --- a/test/integration/request_test.js +++ b/test/integration/request_test.js @@ -1,189 +1,189 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const Request = require("../../lib/request"); -const InvalidArgumentError = require("../../lib/errors/invalid-argument-error"); -const should = require("chai").should(); +const Request = require('../../lib/request'); +const InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); +const should = require('chai').should(); /** * Test `Request` integration. */ -describe("Request integration", function () { - describe("constructor()", function () { - it("should throw an error if `headers` is missing", function () { - try { - new Request({ body: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `headers`"); - } - }); - - it("should throw an error if `method` is missing", function () { - try { - new Request({ body: {}, headers: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `method`"); - } - }); - - it("should throw an error if `query` is missing", function () { - try { - new Request({ body: {}, headers: {}, method: {} }); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `query`"); - } - }); - - it("should set the `body`", function () { - const request = new Request({ - body: "foo", - headers: {}, - method: {}, - query: {}, - }); - - request.body.should.equal("foo"); - }); - - it("should set the `headers`", function () { - const request = new Request({ - body: {}, - headers: { foo: "bar", QuX: "biz" }, - method: {}, - query: {}, - }); - - request.headers.should.eql({ foo: "bar", qux: "biz" }); - }); - - it("should set the `method`", function () { - const request = new Request({ - body: {}, - headers: {}, - method: "biz", - query: {}, - }); - - request.method.should.equal("biz"); - }); - - it("should set the `query`", function () { - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: "baz", - }); - - request.query.should.equal("baz"); - }); - }); - - describe("get()", function () { - it("should return `undefined` if the field does not exist", function () { - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - (undefined === request.get("content-type")).should.be.true; - }); - - it("should return the value if the field exists", function () { - const request = new Request({ - body: {}, - headers: { - "content-type": "text/html; charset=utf-8", - }, - method: {}, - query: {}, - }); - - request.get("Content-Type").should.equal("text/html; charset=utf-8"); - }); - }); - - describe("is()", function () { - it("should accept an array of `types`", function () { - const request = new Request({ - body: {}, - headers: { - "content-type": "application/json", - "transfer-encoding": "chunked", - }, - method: {}, - query: {}, - }); - - request.is(["html", "json"]).should.equal("json"); - }); - - it("should accept multiple `types` as arguments", function () { - const request = new Request({ - body: {}, - headers: { - "content-type": "application/json", - "transfer-encoding": "chunked", - }, - method: {}, - query: {}, - }); - - request.is("html", "json").should.equal("json"); - }); - - it("should return the first matching type", function () { - const request = new Request({ - body: {}, - headers: { - "content-type": "text/html; charset=utf-8", - "transfer-encoding": "chunked", - }, - method: {}, - query: {}, - }); - - request.is("html").should.equal("html"); - }); - - it("should return `false` if none of the `types` match", function () { - const request = new Request({ - body: {}, - headers: { - "content-type": "text/html; charset=utf-8", - "transfer-encoding": "chunked", - }, - method: {}, - query: {}, - }); - - request.is("json").should.be.false; - }); - - it("should return `false` if the request has no body", function () { - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - - request.is("text/html").should.be.false; - }); - }); +describe('Request integration', function () { + describe('constructor()', function () { + it('should throw an error if `headers` is missing', function () { + try { + new Request({ body: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `headers`'); + } + }); + + it('should throw an error if `method` is missing', function () { + try { + new Request({ body: {}, headers: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `method`'); + } + }); + + it('should throw an error if `query` is missing', function () { + try { + new Request({ body: {}, headers: {}, method: {} }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `query`'); + } + }); + + it('should set the `body`', function () { + const request = new Request({ + body: 'foo', + headers: {}, + method: {}, + query: {}, + }); + + request.body.should.equal('foo'); + }); + + it('should set the `headers`', function () { + const request = new Request({ + body: {}, + headers: { foo: 'bar', QuX: 'biz' }, + method: {}, + query: {}, + }); + + request.headers.should.eql({ foo: 'bar', qux: 'biz' }); + }); + + it('should set the `method`', function () { + const request = new Request({ + body: {}, + headers: {}, + method: 'biz', + query: {}, + }); + + request.method.should.equal('biz'); + }); + + it('should set the `query`', function () { + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: 'baz', + }); + + request.query.should.equal('baz'); + }); + }); + + describe('get()', function () { + it('should return `undefined` if the field does not exist', function () { + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + (undefined === request.get('content-type')).should.be.true; + }); + + it('should return the value if the field exists', function () { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'text/html; charset=utf-8', + }, + method: {}, + query: {}, + }); + + request.get('Content-Type').should.equal('text/html; charset=utf-8'); + }); + }); + + describe('is()', function () { + it('should accept an array of `types`', function () { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'application/json', + 'transfer-encoding': 'chunked', + }, + method: {}, + query: {}, + }); + + request.is(['html', 'json']).should.equal('json'); + }); + + it('should accept multiple `types` as arguments', function () { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'application/json', + 'transfer-encoding': 'chunked', + }, + method: {}, + query: {}, + }); + + request.is('html', 'json').should.equal('json'); + }); + + it('should return the first matching type', function () { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'text/html; charset=utf-8', + 'transfer-encoding': 'chunked', + }, + method: {}, + query: {}, + }); + + request.is('html').should.equal('html'); + }); + + it('should return `false` if none of the `types` match', function () { + const request = new Request({ + body: {}, + headers: { + 'content-type': 'text/html; charset=utf-8', + 'transfer-encoding': 'chunked', + }, + method: {}, + query: {}, + }); + + request.is('json').should.be.false; + }); + + it('should return `false` if the request has no body', function () { + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + + request.is('text/html').should.be.false; + }); + }); }); diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js index c42f765a..222ce6a5 100644 --- a/test/integration/response-types/code-response-type_test.js +++ b/test/integration/response-types/code-response-type_test.js @@ -1,70 +1,70 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const CodeResponseType = require("../../../lib/response-types/code-response-type"); -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const should = require("chai").should(); -const url = require("url"); +const CodeResponseType = require('../../../lib/response-types/code-response-type'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const should = require('chai').should(); +const url = require('url'); /** * Test `CodeResponseType` integration. */ -describe("CodeResponseType integration", function () { - describe("constructor()", function () { - it("should throw an error if `code` is missing", function () { - try { - new CodeResponseType(); +describe('CodeResponseType integration', function () { + describe('constructor()', function () { + it('should throw an error if `code` is missing', function () { + try { + new CodeResponseType(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `code`"); - } - }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `code`'); + } + }); - it("should set the `code`", function () { - const responseType = new CodeResponseType("foo"); + it('should set the `code`', function () { + const responseType = new CodeResponseType('foo'); - responseType.code.should.equal("foo"); - }); - }); + responseType.code.should.equal('foo'); + }); + }); - describe("buildRedirectUri()", function () { - it("should throw an error if the `redirectUri` is missing", function () { - const responseType = new CodeResponseType("foo"); + describe('buildRedirectUri()', function () { + it('should throw an error if the `redirectUri` is missing', function () { + const responseType = new CodeResponseType('foo'); - try { - responseType.buildRedirectUri(); + try { + responseType.buildRedirectUri(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `redirectUri`"); - } - }); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `redirectUri`'); + } + }); - it("should return the new redirect uri and set the `code` and `state` in the query", function () { - const responseType = new CodeResponseType("foo"); - const redirectUri = responseType.buildRedirectUri( - "http://example.com/cb", - ); + it('should return the new redirect uri and set the `code` and `state` in the query', function () { + const responseType = new CodeResponseType('foo'); + const redirectUri = responseType.buildRedirectUri( + 'http://example.com/cb', + ); - url.format(redirectUri).should.equal("http://example.com/cb?code=foo"); - }); + url.format(redirectUri).should.equal('http://example.com/cb?code=foo'); + }); - it("should return the new redirect uri and append the `code` and `state` in the query", function () { - const responseType = new CodeResponseType("foo"); - const redirectUri = responseType.buildRedirectUri( - "http://example.com/cb?foo=bar", - ); + it('should return the new redirect uri and append the `code` and `state` in the query', function () { + const responseType = new CodeResponseType('foo'); + const redirectUri = responseType.buildRedirectUri( + 'http://example.com/cb?foo=bar', + ); - url - .format(redirectUri) - .should.equal("http://example.com/cb?foo=bar&code=foo"); - }); - }); + url + .format(redirectUri) + .should.equal('http://example.com/cb?foo=bar&code=foo'); + }); + }); }); diff --git a/test/integration/response_test.js b/test/integration/response_test.js index baef7d3a..cbaf529f 100644 --- a/test/integration/response_test.js +++ b/test/integration/response_test.js @@ -1,81 +1,81 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const Response = require("../../lib/response"); +const Response = require('../../lib/response'); /** * Test `Response` integration. */ -describe("Response integration", function () { - describe("constructor()", function () { - it("should set the `body`", function () { - const response = new Response({ body: "foo", headers: {} }); +describe('Response integration', function () { + describe('constructor()', function () { + it('should set the `body`', function () { + const response = new Response({ body: 'foo', headers: {} }); - response.body.should.equal("foo"); - }); + response.body.should.equal('foo'); + }); - it("should set the `headers`", function () { - const response = new Response({ - body: {}, - headers: { foo: "bar", QuX: "biz" }, - }); + it('should set the `headers`', function () { + const response = new Response({ + body: {}, + headers: { foo: 'bar', QuX: 'biz' }, + }); - response.headers.should.eql({ foo: "bar", qux: "biz" }); - }); + response.headers.should.eql({ foo: 'bar', qux: 'biz' }); + }); - it("should set the `status` to 200", function () { - const response = new Response({ body: {}, headers: {} }); + it('should set the `status` to 200', function () { + const response = new Response({ body: {}, headers: {} }); - response.status.should.equal(200); - }); - }); + response.status.should.equal(200); + }); + }); - describe("get()", function () { - it("should return `undefined` if the field does not exist", function () { - const response = new Response({ body: {}, headers: {} }); + describe('get()', function () { + it('should return `undefined` if the field does not exist', function () { + const response = new Response({ body: {}, headers: {} }); - (undefined === response.get("content-type")).should.be.true; - }); + (undefined === response.get('content-type')).should.be.true; + }); - it("should return the value if the field exists", function () { - const response = new Response({ - body: {}, - headers: { "content-type": "text/html; charset=utf-8" }, - }); + it('should return the value if the field exists', function () { + const response = new Response({ + body: {}, + headers: { 'content-type': 'text/html; charset=utf-8' }, + }); - response.get("Content-Type").should.equal("text/html; charset=utf-8"); - }); - }); + response.get('Content-Type').should.equal('text/html; charset=utf-8'); + }); + }); - describe("redirect()", function () { - it("should set the location header to `url`", function () { - const response = new Response({ body: {}, headers: {} }); + describe('redirect()', function () { + it('should set the location header to `url`', function () { + const response = new Response({ body: {}, headers: {} }); - response.redirect("http://example.com"); + response.redirect('http://example.com'); - response.get("Location").should.equal("http://example.com"); - }); + response.get('Location').should.equal('http://example.com'); + }); - it("should set the `status` to 302", function () { - const response = new Response({ body: {}, headers: {} }); + it('should set the `status` to 302', function () { + const response = new Response({ body: {}, headers: {} }); - response.redirect("http://example.com"); + response.redirect('http://example.com'); - response.status.should.equal(302); - }); - }); + response.status.should.equal(302); + }); + }); - describe("set()", function () { - it("should set the `field`", function () { - const response = new Response({ body: {}, headers: {} }); + describe('set()', function () { + it('should set the `field`', function () { + const response = new Response({ body: {}, headers: {} }); - response.set("foo", "bar"); + response.set('foo', 'bar'); - response.headers.should.eql({ foo: "bar" }); - }); - }); + response.headers.should.eql({ foo: 'bar' }); + }); + }); }); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index ba6cde0f..53617806 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -1,248 +1,248 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const InvalidArgumentError = require("../../lib/errors/invalid-argument-error"); -const Model = require("../../lib/model"); -const Request = require("../../lib/request"); -const Response = require("../../lib/response"); -const Server = require("../../lib/server"); -const should = require("chai").should(); +const InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); +const Model = require('../../lib/model'); +const Request = require('../../lib/request'); +const Response = require('../../lib/response'); +const Server = require('../../lib/server'); +const should = require('chai').should(); /** * Test `Server` integration. */ -describe("Server integration", function () { - describe("constructor()", function () { - it("should throw an error if `model` is missing", function () { - [null, undefined, {}].forEach((options) => { - try { - new Server(options); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `model`"); - } - }); - }); - - it("should set the `model`", function () { - const model = Model.from({}); - const server = new Server({ model: model }); - - server.options.model.should.equal(model); - }); - }); - - describe("authenticate()", function () { - it("should set the default `options`", async function () { - const model = Model.from({ - getAccessToken: function () { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - }); - const server = new Server({ model: model }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await server.authenticate(request, response); - } catch (e) { - server.addAcceptedScopesHeader.should.be.true; - server.addAuthorizedScopesHeader.should.be.true; - server.allowBearerTokensInQueryString.should.be.false; - should.fail(); - } - }); - - it("should return a promise", function () { - const model = Model.from({ - getAccessToken: async function (token) { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - }); - const server = new Server({ model: model }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - const handler = server.authenticate(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - }); - - describe("authorize()", function () { - it("should set the default `options`", async function () { - const model = Model.from({ - getAccessToken: function () { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: function () { - return { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - }, - saveAuthorizationCode: function () { - return { authorizationCode: 123 }; - }, - }); - const server = new Server({ model: model }); - const request = new Request({ - body: { - client_id: 1234, - client_secret: "secret", - response_type: "code", - }, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: { state: "foobar" }, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await server.authorize(request, response); - } catch (e) { - server.allowEmptyState.should.be.false; - server.authorizationCodeLifetime.should.equal(300); - should.fail(); - } - }); - - it("should return a promise", function () { - const model = Model.from({ - getAccessToken: function () { - return { - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }; - }, - getClient: function () { - return { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - }, - saveAuthorizationCode: function () { - return { authorizationCode: 123 }; - }, - }); - const server = new Server({ model: model }); - const request = new Request({ - body: { - client_id: 1234, - client_secret: "secret", - response_type: "code", - }, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: { state: "foobar" }, - }); - const response = new Response({ body: {}, headers: {} }); - const handler = server.authorize(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - }); - - describe("token()", function () { - it("should set the default `options`", async function () { - const model = Model.from({ - getClient: function () { - return { grants: ["password"] }; - }, - getUser: function () { - return {}; - }, - saveToken: function () { - return { accessToken: 1234, client: {}, user: {} }; - }, - validateScope: function () { - return ["foo"]; - }, - }); - const server = new Server({ model: model }); - const request = new Request({ - body: { - client_id: 1234, - client_secret: "secret", - grant_type: "password", - username: "foo", - password: "pass", - scope: "foo", - }, - headers: { - "content-type": "application/x-www-form-urlencoded", - "transfer-encoding": "chunked", - }, - method: "POST", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - - try { - await server.token(request, response); - } catch (e) { - server.accessTokenLifetime.should.equal(3600); - server.refreshTokenLifetime.should.equal(1209600); - should.fail(); - } - }); - - it("should return a promise", function () { - const model = Model.from({ - getClient: function () { - return { grants: ["password"] }; - }, - getUser: function () { - return {}; - }, - saveToken: function () { - return { accessToken: 1234, client: {}, user: {} }; - }, - }); - const server = new Server({ model: model }); - const request = new Request({ - body: { - client_id: 1234, - client_secret: "secret", - grant_type: "password", - username: "foo", - password: "pass", - }, - headers: { - "content-type": "application/x-www-form-urlencoded", - "transfer-encoding": "chunked", - }, - method: "POST", - query: {}, - }); - const response = new Response({ body: {}, headers: {} }); - const handler = server.token(request, response); - - handler.should.be.an.instanceOf(Promise); - }); - }); +describe('Server integration', function () { + describe('constructor()', function () { + it('should throw an error if `model` is missing', function () { + [null, undefined, {}].forEach((options) => { + try { + new Server(options); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `model`'); + } + }); + }); + + it('should set the `model`', function () { + const model = Model.from({}); + const server = new Server({ model: model }); + + server.options.model.should.equal(model); + }); + }); + + describe('authenticate()', function () { + it('should set the default `options`', async function () { + const model = Model.from({ + getAccessToken: function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await server.authenticate(request, response); + } catch (e) { + server.addAcceptedScopesHeader.should.be.true; + server.addAuthorizedScopesHeader.should.be.true; + server.allowBearerTokensInQueryString.should.be.false; + should.fail(); + } + }); + + it('should return a promise', function () { + const model = Model.from({ + getAccessToken: async function (token) { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + const handler = server.authenticate(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + }); + + describe('authorize()', function () { + it('should set the default `options`', async function () { + const model = Model.from({ + getAccessToken: function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: function () { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: function () { + return { authorizationCode: 123 }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + response_type: 'code', + }, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: { state: 'foobar' }, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await server.authorize(request, response); + } catch (e) { + server.allowEmptyState.should.be.false; + server.authorizationCodeLifetime.should.equal(300); + should.fail(); + } + }); + + it('should return a promise', function () { + const model = Model.from({ + getAccessToken: function () { + return { + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }; + }, + getClient: function () { + return { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + }, + saveAuthorizationCode: function () { + return { authorizationCode: 123 }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + response_type: 'code', + }, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: { state: 'foobar' }, + }); + const response = new Response({ body: {}, headers: {} }); + const handler = server.authorize(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + }); + + describe('token()', function () { + it('should set the default `options`', async function () { + const model = Model.from({ + getClient: function () { + return { grants: ['password'] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return { accessToken: 1234, client: {}, user: {} }; + }, + validateScope: function () { + return ['foo']; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + grant_type: 'password', + username: 'foo', + password: 'pass', + scope: 'foo', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + + try { + await server.token(request, response); + } catch (e) { + server.accessTokenLifetime.should.equal(3600); + server.refreshTokenLifetime.should.equal(1209600); + should.fail(); + } + }); + + it('should return a promise', function () { + const model = Model.from({ + getClient: function () { + return { grants: ['password'] }; + }, + getUser: function () { + return {}; + }, + saveToken: function () { + return { accessToken: 1234, client: {}, user: {} }; + }, + }); + const server = new Server({ model: model }); + const request = new Request({ + body: { + client_id: 1234, + client_secret: 'secret', + grant_type: 'password', + username: 'foo', + password: 'pass', + }, + headers: { + 'content-type': 'application/x-www-form-urlencoded', + 'transfer-encoding': 'chunked', + }, + method: 'POST', + query: {}, + }); + const response = new Response({ body: {}, headers: {} }); + const handler = server.token(request, response); + + handler.should.be.an.instanceOf(Promise); + }); + }); }); diff --git a/test/integration/token-types/bearer-token-type_test.js b/test/integration/token-types/bearer-token-type_test.js index e3a862ba..0b87d84d 100644 --- a/test/integration/token-types/bearer-token-type_test.js +++ b/test/integration/token-types/bearer-token-type_test.js @@ -1,93 +1,93 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const BearerTokenType = require("../../../lib/token-types/bearer-token-type"); -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const should = require("chai").should(); +const BearerTokenType = require('../../../lib/token-types/bearer-token-type'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const should = require('chai').should(); /** * Test `BearerTokenType` integration. */ -describe("BearerTokenType integration", function () { - describe("constructor()", function () { - it("should throw an error if `accessToken` is missing", function () { - try { - new BearerTokenType(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `accessToken`"); - } - }); - - it("should set the `accessToken`", function () { - const responseType = new BearerTokenType("foo", "bar"); - - responseType.accessToken.should.equal("foo"); - }); - - it("should set the `accessTokenLifetime`", function () { - const responseType = new BearerTokenType("foo", "bar"); - - responseType.accessTokenLifetime.should.equal("bar"); - }); - - it("should set the `refreshToken`", function () { - const responseType = new BearerTokenType("foo", "bar", "biz"); - - responseType.refreshToken.should.equal("biz"); - }); - }); - - describe("valueOf()", function () { - it("should return the value representation", function () { - const responseType = new BearerTokenType("foo", "bar"); - const value = responseType.valueOf(); - - value.should.eql({ - access_token: "foo", - expires_in: "bar", - token_type: "Bearer", - }); - }); - - it("should not include the `expires_in` if not given", function () { - const responseType = new BearerTokenType("foo"); - const value = responseType.valueOf(); - - value.should.eql({ - access_token: "foo", - token_type: "Bearer", - }); - }); - - it("should set `refresh_token` if `refreshToken` is defined", function () { - const responseType = new BearerTokenType("foo", "bar", "biz"); - const value = responseType.valueOf(); - - value.should.eql({ - access_token: "foo", - expires_in: "bar", - refresh_token: "biz", - token_type: "Bearer", - }); - }); - - it("should set `expires_in` if `accessTokenLifetime` is defined", function () { - const responseType = new BearerTokenType("foo", "bar", "biz"); - const value = responseType.valueOf(); - - value.should.eql({ - access_token: "foo", - expires_in: "bar", - refresh_token: "biz", - token_type: "Bearer", - }); - }); - }); +describe('BearerTokenType integration', function () { + describe('constructor()', function () { + it('should throw an error if `accessToken` is missing', function () { + try { + new BearerTokenType(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessToken`'); + } + }); + + it('should set the `accessToken`', function () { + const responseType = new BearerTokenType('foo', 'bar'); + + responseType.accessToken.should.equal('foo'); + }); + + it('should set the `accessTokenLifetime`', function () { + const responseType = new BearerTokenType('foo', 'bar'); + + responseType.accessTokenLifetime.should.equal('bar'); + }); + + it('should set the `refreshToken`', function () { + const responseType = new BearerTokenType('foo', 'bar', 'biz'); + + responseType.refreshToken.should.equal('biz'); + }); + }); + + describe('valueOf()', function () { + it('should return the value representation', function () { + const responseType = new BearerTokenType('foo', 'bar'); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + expires_in: 'bar', + token_type: 'Bearer', + }); + }); + + it('should not include the `expires_in` if not given', function () { + const responseType = new BearerTokenType('foo'); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + token_type: 'Bearer', + }); + }); + + it('should set `refresh_token` if `refreshToken` is defined', function () { + const responseType = new BearerTokenType('foo', 'bar', 'biz'); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + expires_in: 'bar', + refresh_token: 'biz', + token_type: 'Bearer', + }); + }); + + it('should set `expires_in` if `accessTokenLifetime` is defined', function () { + const responseType = new BearerTokenType('foo', 'bar', 'biz'); + const value = responseType.valueOf(); + + value.should.eql({ + access_token: 'foo', + expires_in: 'bar', + refresh_token: 'biz', + token_type: 'Bearer', + }); + }); + }); }); diff --git a/test/integration/utils/token-util_test.js b/test/integration/utils/token-util_test.js index 69594a8a..c3e119ea 100644 --- a/test/integration/utils/token-util_test.js +++ b/test/integration/utils/token-util_test.js @@ -1,20 +1,20 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const TokenUtil = require("../../../lib/utils/token-util"); +const TokenUtil = require('../../../lib/utils/token-util'); /** * Test `TokenUtil` integration. */ -describe("TokenUtil integration", function () { - describe("generateRandomToken()", function () { - it("should return a sha-256 token", async function () { - const token = await TokenUtil.generateRandomToken(); - token.should.be.a.sha256(); - }); - }); +describe('TokenUtil integration', function () { + describe('generateRandomToken()', function () { + it('should return a sha-256 token', async function () { + const token = await TokenUtil.generateRandomToken(); + token.should.be.a.sha256(); + }); + }); }); diff --git a/test/unit/errors/oauth-error_test.js b/test/unit/errors/oauth-error_test.js index 28ade68a..1939793d 100644 --- a/test/unit/errors/oauth-error_test.js +++ b/test/unit/errors/oauth-error_test.js @@ -1,59 +1,59 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const { describe, it } = require("mocha"); -const should = require("chai").should(); -const OAuthError = require("../../../lib/errors/oauth-error"); +const { describe, it } = require('mocha'); +const should = require('chai').should(); +const OAuthError = require('../../../lib/errors/oauth-error'); /** * Test `OAuthError`. */ -describe("OAuthError", function () { - describe("constructor()", function () { - it("should get `captureStackTrace`", function () { - const errorFn = function () { - throw new OAuthError("test", { name: "test_error", foo: "bar" }); - }; - - try { - errorFn(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(OAuthError); - e.name.should.equal("test_error"); - e.foo.should.equal("bar"); - e.message.should.equal("test"); - e.code.should.equal(500); - e.stack.should.not.be.null; - e.stack.should.not.be.undefined; - e.stack.should.include("oauth-error_test.js"); - e.stack.should.include("19"); //error lineNUmber - } - }); - }); - it("supports undefined properties", function () { - const errorFn = function () { - throw new OAuthError("test"); - }; - - try { - errorFn(); - - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(OAuthError); - e.name.should.equal("Error"); - e.message.should.equal("test"); - e.code.should.equal(500); - e.stack.should.not.be.null; - e.stack.should.not.be.undefined; - e.stack.should.include("oauth-error_test.js"); - e.stack.should.include("41"); //error lineNUmber - } - }); +describe('OAuthError', function () { + describe('constructor()', function () { + it('should get `captureStackTrace`', function () { + const errorFn = function () { + throw new OAuthError('test', { name: 'test_error', foo: 'bar' }); + }; + + try { + errorFn(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(OAuthError); + e.name.should.equal('test_error'); + e.foo.should.equal('bar'); + e.message.should.equal('test'); + e.code.should.equal(500); + e.stack.should.not.be.null; + e.stack.should.not.be.undefined; + e.stack.should.include('oauth-error_test.js'); + e.stack.should.include('19'); //error lineNUmber + } + }); + }); + it('supports undefined properties', function () { + const errorFn = function () { + throw new OAuthError('test'); + }; + + try { + errorFn(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(OAuthError); + e.name.should.equal('Error'); + e.message.should.equal('test'); + e.code.should.equal(500); + e.stack.should.not.be.null; + e.stack.should.not.be.undefined; + e.stack.should.include('oauth-error_test.js'); + e.stack.should.include('41'); //error lineNUmber + } + }); }); diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js index 432b3bb1..946aa1ee 100644 --- a/test/unit/grant-types/abstract-grant-type_test.js +++ b/test/unit/grant-types/abstract-grant-type_test.js @@ -1,62 +1,62 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AbstractGrantType = require("../../../lib/grant-types/abstract-grant-type"); -const Model = require("../../../lib/model"); -const sinon = require("sinon"); -const should = require("chai").should(); +const AbstractGrantType = require('../../../lib/grant-types/abstract-grant-type'); +const Model = require('../../../lib/model'); +const sinon = require('sinon'); +const should = require('chai').should(); /** * Test `AbstractGrantType`. */ -describe("AbstractGrantType", function () { - describe("generateAccessToken()", function () { - it("should call `model.generateAccessToken()`", function () { - const model = Model.from({ - generateAccessToken: sinon - .stub() - .returns({ client: {}, expiresAt: new Date(), user: {} }), - }); - const handler = new AbstractGrantType({ - accessTokenLifetime: 120, - model: model, - }); +describe('AbstractGrantType', function () { + describe('generateAccessToken()', function () { + it('should call `model.generateAccessToken()`', function () { + const model = Model.from({ + generateAccessToken: sinon + .stub() + .returns({ client: {}, expiresAt: new Date(), user: {} }), + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 120, + model: model, + }); - return handler - .generateAccessToken() - .then(function () { - model.generateAccessToken.callCount.should.equal(1); - model.generateAccessToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .generateAccessToken() + .then(function () { + model.generateAccessToken.callCount.should.equal(1); + model.generateAccessToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("generateRefreshToken()", function () { - it("should call `model.generateRefreshToken()`", function () { - const model = Model.from({ - generateRefreshToken: sinon.stub().returns({ - client: {}, - expiresAt: new Date(new Date() / 2), - user: {}, - }), - }); - const handler = new AbstractGrantType({ - accessTokenLifetime: 120, - model: model, - }); + describe('generateRefreshToken()', function () { + it('should call `model.generateRefreshToken()`', function () { + const model = Model.from({ + generateRefreshToken: sinon.stub().returns({ + client: {}, + expiresAt: new Date(new Date() / 2), + user: {}, + }), + }); + const handler = new AbstractGrantType({ + accessTokenLifetime: 120, + model: model, + }); - return handler - .generateRefreshToken() - .then(function () { - model.generateRefreshToken.callCount.should.equal(1); - model.generateRefreshToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .generateRefreshToken() + .then(function () { + model.generateRefreshToken.callCount.should.equal(1); + model.generateRefreshToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index df7af5a0..49c15485 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -1,212 +1,212 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AuthorizationCodeGrantType = require("../../../lib/grant-types/authorization-code-grant-type"); -const Request = require("../../../lib/request"); -const Model = require("../../../lib/model"); -const sinon = require("sinon"); -const should = require("chai").should(); -const stringUtil = require("../../../lib/utils/string-util"); -const crypto = require("crypto"); +const AuthorizationCodeGrantType = require('../../../lib/grant-types/authorization-code-grant-type'); +const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); +const sinon = require('sinon'); +const should = require('chai').should(); +const stringUtil = require('../../../lib/utils/string-util'); +const crypto = require('crypto'); /** * Test `AuthorizationCodeGrantType`. */ -describe("AuthorizationCodeGrantType", function () { - describe("getAuthorizationCode()", function () { - it("should call `model.getAuthorizationCode()`", function () { - const model = Model.from({ - getAuthorizationCode: sinon.stub().returns({ - authorizationCode: 12345, - client: {}, - expiresAt: new Date(new Date() * 2), - user: {}, - }), - revokeAuthorizationCode: function () {}, - saveToken: function () {}, - }); - const handler = new AuthorizationCodeGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: { code: 12345 }, - headers: {}, - method: {}, - query: {}, - }); - const client = {}; +describe('AuthorizationCodeGrantType', function () { + describe('getAuthorizationCode()', function () { + it('should call `model.getAuthorizationCode()`', function () { + const model = Model.from({ + getAuthorizationCode: sinon.stub().returns({ + authorizationCode: 12345, + client: {}, + expiresAt: new Date(new Date() * 2), + user: {}, + }), + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { code: 12345 }, + headers: {}, + method: {}, + query: {}, + }); + const client = {}; - return handler - .getAuthorizationCode(request, client) - .then(function () { - model.getAuthorizationCode.callCount.should.equal(1); - model.getAuthorizationCode.firstCall.args.should.have.length(1); - model.getAuthorizationCode.firstCall.args[0].should.equal(12345); - model.getAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .getAuthorizationCode(request, client) + .then(function () { + model.getAuthorizationCode.callCount.should.equal(1); + model.getAuthorizationCode.firstCall.args.should.have.length(1); + model.getAuthorizationCode.firstCall.args[0].should.equal(12345); + model.getAuthorizationCode.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("revokeAuthorizationCode()", function () { - it("should call `model.revokeAuthorizationCode()`", function () { - const model = Model.from({ - getAuthorizationCode: function () {}, - revokeAuthorizationCode: sinon.stub().returns(true), - saveToken: function () {}, - }); - const handler = new AuthorizationCodeGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const authorizationCode = {}; + describe('revokeAuthorizationCode()', function () { + it('should call `model.revokeAuthorizationCode()`', function () { + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: sinon.stub().returns(true), + saveToken: function () {}, + }); + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const authorizationCode = {}; - return handler - .revokeAuthorizationCode(authorizationCode) - .then(function () { - model.revokeAuthorizationCode.callCount.should.equal(1); - model.revokeAuthorizationCode.firstCall.args.should.have.length(1); - model.revokeAuthorizationCode.firstCall.args[0].should.equal( - authorizationCode, - ); - model.revokeAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .revokeAuthorizationCode(authorizationCode) + .then(function () { + model.revokeAuthorizationCode.callCount.should.equal(1); + model.revokeAuthorizationCode.firstCall.args.should.have.length(1); + model.revokeAuthorizationCode.firstCall.args[0].should.equal( + authorizationCode, + ); + model.revokeAuthorizationCode.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("saveToken()", function () { - it("should call `model.saveToken()`", function () { - const client = {}; - const user = {}; - const model = Model.from({ - getAuthorizationCode: function () {}, - revokeAuthorizationCode: function () {}, - saveToken: sinon.stub().returns(true), - }); - const handler = new AuthorizationCodeGrantType({ - accessTokenLifetime: 120, - model: model, - }); + describe('saveToken()', function () { + it('should call `model.saveToken()`', function () { + const client = {}; + const user = {}; + const model = Model.from({ + getAuthorizationCode: function () {}, + revokeAuthorizationCode: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new AuthorizationCodeGrantType({ + accessTokenLifetime: 120, + model: model, + }); - sinon.stub(handler, "validateScope").returns(["foobiz"]); - sinon - .stub(handler, "generateAccessToken") - .returns(Promise.resolve("foo")); - sinon - .stub(handler, "generateRefreshToken") - .returns(Promise.resolve("bar")); - sinon - .stub(handler, "getAccessTokenExpiresAt") - .returns(Promise.resolve("biz")); - sinon - .stub(handler, "getRefreshTokenExpiresAt") - .returns(Promise.resolve("baz")); + sinon.stub(handler, 'validateScope').returns(['foobiz']); + sinon + .stub(handler, 'generateAccessToken') + .returns(Promise.resolve('foo')); + sinon + .stub(handler, 'generateRefreshToken') + .returns(Promise.resolve('bar')); + sinon + .stub(handler, 'getAccessTokenExpiresAt') + .returns(Promise.resolve('biz')); + sinon + .stub(handler, 'getRefreshTokenExpiresAt') + .returns(Promise.resolve('baz')); - return handler - .saveToken(user, client, "foobar", ["foobiz"]) - .then(function () { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ - accessToken: "foo", - authorizationCode: "foobar", - accessTokenExpiresAt: "biz", - refreshToken: "bar", - refreshTokenExpiresAt: "baz", - scope: ["foobiz"], - }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .saveToken(user, client, 'foobar', ['foobiz']) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + authorizationCode: 'foobar', + accessTokenExpiresAt: 'biz', + refreshToken: 'bar', + refreshTokenExpiresAt: 'baz', + scope: ['foobiz'], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("with PKCE", function () { - // xxx: the tests for `getAuthorizationCode` are removed, because PKCE is now validated - // in the handle method to ensure token revocation is performed before PKCE validation. + describe('with PKCE', function () { + // xxx: the tests for `getAuthorizationCode` are removed, because PKCE is now validated + // in the handle method to ensure token revocation is performed before PKCE validation. - it("should return an auth code when `code_verifier` is valid with S256 code challenge method", function () { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar", isPublic: true }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: "S256", - codeChallenge: stringUtil.base64URLEncode( - crypto.createHash("sha256").update(codeVerifier).digest(), - ), - }; - const client = { id: "foobar", isPublic: true }; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - revokeAuthorizationCode: function () {}, - saveToken: function () {}, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345, code_verifier: codeVerifier }, - headers: {}, - method: {}, - query: {}, - }); + it('should return an auth code when `code_verifier` is valid with S256 code challenge method', function () { + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar', isPublic: true }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: 'S256', + codeChallenge: stringUtil.base64URLEncode( + crypto.createHash('sha256').update(codeVerifier).digest(), + ), + }; + const client = { id: 'foobar', isPublic: true }; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345, code_verifier: codeVerifier }, + headers: {}, + method: {}, + query: {}, + }); - return grantType - .getAuthorizationCode(request, client) - .then(function (data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); + return grantType + .getAuthorizationCode(request, client) + .then(function (data) { + data.should.equal(authorizationCode); + }) + .catch(should.fail); + }); - it("should return an auth code when `code_verifier` is valid with plain code challenge method", function () { - const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); - const authorizationCode = { - authorizationCode: 12345, - client: { id: "foobar" }, - expiresAt: new Date(new Date().getTime() * 2), - user: {}, - codeChallengeMethod: "plain", - codeChallenge: codeVerifier, - }; - const client = { id: "foobar", isPublic: true }; - const model = Model.from({ - getAuthorizationCode: function () { - return authorizationCode; - }, - revokeAuthorizationCode: function () {}, - saveToken: function () {}, - }); - const grantType = new AuthorizationCodeGrantType({ - accessTokenLifetime: 123, - model: model, - }); - const request = new Request({ - body: { code: 12345, code_verifier: codeVerifier }, - headers: {}, - method: {}, - query: {}, - }); + it('should return an auth code when `code_verifier` is valid with plain code challenge method', function () { + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); + const authorizationCode = { + authorizationCode: 12345, + client: { id: 'foobar' }, + expiresAt: new Date(new Date().getTime() * 2), + user: {}, + codeChallengeMethod: 'plain', + codeChallenge: codeVerifier, + }; + const client = { id: 'foobar', isPublic: true }; + const model = Model.from({ + getAuthorizationCode: function () { + return authorizationCode; + }, + revokeAuthorizationCode: function () {}, + saveToken: function () {}, + }); + const grantType = new AuthorizationCodeGrantType({ + accessTokenLifetime: 123, + model: model, + }); + const request = new Request({ + body: { code: 12345, code_verifier: codeVerifier }, + headers: {}, + method: {}, + query: {}, + }); - return grantType - .getAuthorizationCode(request, client) - .then(function (data) { - data.should.equal(authorizationCode); - }) - .catch(should.fail); - }); - }); + return grantType + .getAuthorizationCode(request, client) + .then(function (data) { + data.should.equal(authorizationCode); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/grant-types/client-credentials-grant-type_test.js b/test/unit/grant-types/client-credentials-grant-type_test.js index a2ef1563..fb3042dd 100644 --- a/test/unit/grant-types/client-credentials-grant-type_test.js +++ b/test/unit/grant-types/client-credentials-grant-type_test.js @@ -1,75 +1,75 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const ClientCredentialsGrantType = require("../../../lib/grant-types/client-credentials-grant-type"); -const Model = require("../../../lib/model"); -const sinon = require("sinon"); -const should = require("chai").should(); +const ClientCredentialsGrantType = require('../../../lib/grant-types/client-credentials-grant-type'); +const Model = require('../../../lib/model'); +const sinon = require('sinon'); +const should = require('chai').should(); /** * Test `ClientCredentialsGrantType`. */ -describe("ClientCredentialsGrantType", function () { - describe("getUserFromClient()", function () { - it("should call `model.getUserFromClient()`", function () { - const model = Model.from({ - getUserFromClient: sinon.stub().returns(true), - saveToken: function () {}, - }); - const handler = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const client = {}; +describe('ClientCredentialsGrantType', function () { + describe('getUserFromClient()', function () { + it('should call `model.getUserFromClient()`', function () { + const model = Model.from({ + getUserFromClient: sinon.stub().returns(true), + saveToken: function () {}, + }); + const handler = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const client = {}; - return handler - .getUserFromClient(client) - .then(function () { - model.getUserFromClient.callCount.should.equal(1); - model.getUserFromClient.firstCall.args.should.have.length(1); - model.getUserFromClient.firstCall.args[0].should.equal(client); - model.getUserFromClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .getUserFromClient(client) + .then(function () { + model.getUserFromClient.callCount.should.equal(1); + model.getUserFromClient.firstCall.args.should.have.length(1); + model.getUserFromClient.firstCall.args[0].should.equal(client); + model.getUserFromClient.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("saveToken()", function () { - it("should call `model.saveToken()`", function () { - const client = {}; - const user = {}; - const model = Model.from({ - getUserFromClient: function () {}, - saveToken: sinon.stub().returns(true), - }); - const handler = new ClientCredentialsGrantType({ - accessTokenLifetime: 120, - model: model, - }); + describe('saveToken()', function () { + it('should call `model.saveToken()`', function () { + const client = {}; + const user = {}; + const model = Model.from({ + getUserFromClient: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new ClientCredentialsGrantType({ + accessTokenLifetime: 120, + model: model, + }); - sinon.stub(handler, "validateScope").returns(["foobar"]); - sinon.stub(handler, "generateAccessToken").returns("foo"); - sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); + sinon.stub(handler, 'validateScope').returns(['foobar']); + sinon.stub(handler, 'generateAccessToken').returns('foo'); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); - return handler - .saveToken(user, client, ["foobar"]) - .then(function () { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ - accessToken: "foo", - accessTokenExpiresAt: "biz", - scope: ["foobar"], - }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .saveToken(user, client, ['foobar']) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + scope: ['foobar'], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/grant-types/password-grant-type_test.js b/test/unit/grant-types/password-grant-type_test.js index 7d79ca75..d2e31ce9 100644 --- a/test/unit/grant-types/password-grant-type_test.js +++ b/test/unit/grant-types/password-grant-type_test.js @@ -1,87 +1,87 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const PasswordGrantType = require("../../../lib/grant-types/password-grant-type"); -const Request = require("../../../lib/request"); -const Model = require("../../../lib/model"); -const sinon = require("sinon"); -const should = require("chai").should(); +const PasswordGrantType = require('../../../lib/grant-types/password-grant-type'); +const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); +const sinon = require('sinon'); +const should = require('chai').should(); /** * Test `PasswordGrantType`. */ -describe("PasswordGrantType", function () { - describe("getUser()", function () { - it("should call `model.getUser()`", function () { - const model = Model.from({ - getUser: sinon.stub().returns(true), - saveToken: function () {}, - }); - const client = { id: "foobar" }; - const handler = new PasswordGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: { username: "foo", password: "bar" }, - headers: {}, - method: {}, - query: {}, - }); +describe('PasswordGrantType', function () { + describe('getUser()', function () { + it('should call `model.getUser()`', function () { + const model = Model.from({ + getUser: sinon.stub().returns(true), + saveToken: function () {}, + }); + const client = { id: 'foobar' }; + const handler = new PasswordGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { username: 'foo', password: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); - return handler - .getUser(request, client) - .then(function () { - model.getUser.callCount.should.equal(1); - model.getUser.firstCall.args.should.have.length(3); - model.getUser.firstCall.args[0].should.equal("foo"); - model.getUser.firstCall.args[1].should.equal("bar"); - model.getUser.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .getUser(request, client) + .then(function () { + model.getUser.callCount.should.equal(1); + model.getUser.firstCall.args.should.have.length(3); + model.getUser.firstCall.args[0].should.equal('foo'); + model.getUser.firstCall.args[1].should.equal('bar'); + model.getUser.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("saveToken()", function () { - it("should call `model.saveToken()`", function () { - const client = {}; - const user = {}; - const model = Model.from({ - getUser: function () {}, - saveToken: sinon.stub().returns(true), - }); - const handler = new PasswordGrantType({ - accessTokenLifetime: 120, - model: model, - }); + describe('saveToken()', function () { + it('should call `model.saveToken()`', function () { + const client = {}; + const user = {}; + const model = Model.from({ + getUser: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new PasswordGrantType({ + accessTokenLifetime: 120, + model: model, + }); - sinon.stub(handler, "validateScope").returns(["foobar"]); - sinon.stub(handler, "generateAccessToken").returns("foo"); - sinon.stub(handler, "generateRefreshToken").returns("bar"); - sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); - sinon.stub(handler, "getRefreshTokenExpiresAt").returns("baz"); + sinon.stub(handler, 'validateScope').returns(['foobar']); + sinon.stub(handler, 'generateAccessToken').returns('foo'); + sinon.stub(handler, 'generateRefreshToken').returns('bar'); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - return handler - .saveToken(user, client, ["foobar"]) - .then(function () { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ - accessToken: "foo", - accessTokenExpiresAt: "biz", - refreshToken: "bar", - refreshTokenExpiresAt: "baz", - scope: ["foobar"], - }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .saveToken(user, client, ['foobar']) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + refreshToken: 'bar', + refreshTokenExpiresAt: 'baz', + scope: ['foobar'], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js index 4b0dce11..68ca43cb 100644 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ b/test/unit/grant-types/refresh-token-grant-type_test.js @@ -1,289 +1,289 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const RefreshTokenGrantType = require("../../../lib/grant-types/refresh-token-grant-type"); -const Request = require("../../../lib/request"); -const Model = require("../../../lib/model"); -const sinon = require("sinon"); -const should = require("chai").should(); +const RefreshTokenGrantType = require('../../../lib/grant-types/refresh-token-grant-type'); +const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); +const sinon = require('sinon'); +const should = require('chai').should(); /** * Test `RefreshTokenGrantType`. */ -describe("RefreshTokenGrantType", function () { - describe("handle()", function () { - it("should revoke the previous token", function () { - const token = { accessToken: "foo", client: {}, user: {} }; - const model = Model.from({ - getRefreshToken: function () { - return token; - }, - saveToken: function () { - return { accessToken: "bar", client: {}, user: {} }; - }, - revokeToken: sinon.stub().returns({ - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }), - }); - const handler = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: { refresh_token: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - const client = {}; +describe('RefreshTokenGrantType', function () { + describe('handle()', function () { + it('should revoke the previous token', function () { + const token = { accessToken: 'foo', client: {}, user: {} }; + const model = Model.from({ + getRefreshToken: function () { + return token; + }, + saveToken: function () { + return { accessToken: 'bar', client: {}, user: {} }; + }, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { refresh_token: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + const client = {}; - return handler - .handle(request, client) - .then(function () { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .handle(request, client) + .then(function () { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("getRefreshToken()", function () { - it("should call `model.getRefreshToken()`", function () { - const model = Model.from({ - getRefreshToken: sinon - .stub() - .returns({ accessToken: "foo", client: {}, user: {} }), - saveToken: function () {}, - revokeToken: function () {}, - }); - const handler = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const request = new Request({ - body: { refresh_token: "bar" }, - headers: {}, - method: {}, - query: {}, - }); - const client = {}; + describe('getRefreshToken()', function () { + it('should call `model.getRefreshToken()`', function () { + const model = Model.from({ + getRefreshToken: sinon + .stub() + .returns({ accessToken: 'foo', client: {}, user: {} }), + saveToken: function () {}, + revokeToken: function () {}, + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const request = new Request({ + body: { refresh_token: 'bar' }, + headers: {}, + method: {}, + query: {}, + }); + const client = {}; - return handler - .getRefreshToken(request, client) - .then(function () { - model.getRefreshToken.callCount.should.equal(1); - model.getRefreshToken.firstCall.args.should.have.length(1); - model.getRefreshToken.firstCall.args[0].should.equal("bar"); - model.getRefreshToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .getRefreshToken(request, client) + .then(function () { + model.getRefreshToken.callCount.should.equal(1); + model.getRefreshToken.firstCall.args.should.have.length(1); + model.getRefreshToken.firstCall.args[0].should.equal('bar'); + model.getRefreshToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("revokeToken()", function () { - it("should call `model.revokeToken()`", function () { - const model = Model.from({ - getRefreshToken: function () {}, - revokeToken: sinon.stub().returns({ - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }), - saveToken: function () {}, - }); - const handler = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model: model, - }); - const token = {}; + describe('revokeToken()', function () { + it('should call `model.revokeToken()`', function () { + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), + saveToken: function () {}, + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + }); + const token = {}; - return handler - .revokeToken(token) - .then(function () { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); + return handler + .revokeToken(token) + .then(function () { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); - it("should not call `model.revokeToken()`", function () { - const model = Model.from({ - getRefreshToken: function () {}, - revokeToken: sinon.stub().returns({ - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }), - saveToken: function () {}, - }); - const handler = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model: model, - alwaysIssueNewRefreshToken: false, - }); - const token = {}; + it('should not call `model.revokeToken()`', function () { + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), + saveToken: function () {}, + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + alwaysIssueNewRefreshToken: false, + }); + const token = {}; - return handler - .revokeToken(token) - .then(function () { - model.revokeToken.callCount.should.equal(0); - }) - .catch(should.fail); - }); + return handler + .revokeToken(token) + .then(function () { + model.revokeToken.callCount.should.equal(0); + }) + .catch(should.fail); + }); - it("should not call `model.revokeToken()`", function () { - const model = Model.from({ - getRefreshToken: function () {}, - revokeToken: sinon.stub().returns({ - accessToken: "foo", - client: {}, - refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }), - saveToken: function () {}, - }); - const handler = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model: model, - alwaysIssueNewRefreshToken: true, - }); - const token = {}; + it('should not call `model.revokeToken()`', function () { + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: sinon.stub().returns({ + accessToken: 'foo', + client: {}, + refreshTokenExpiresAt: new Date(new Date() / 2), + user: {}, + }), + saveToken: function () {}, + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + alwaysIssueNewRefreshToken: true, + }); + const token = {}; - return handler - .revokeToken(token) - .then(function () { - model.revokeToken.callCount.should.equal(1); - model.revokeToken.firstCall.args.should.have.length(1); - model.revokeToken.firstCall.args[0].should.equal(token); - model.revokeToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .revokeToken(token) + .then(function () { + model.revokeToken.callCount.should.equal(1); + model.revokeToken.firstCall.args.should.have.length(1); + model.revokeToken.firstCall.args[0].should.equal(token); + model.revokeToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("saveToken()", function () { - it("should call `model.saveToken()`", function () { - const client = {}; - const user = {}; - const model = Model.from({ - getRefreshToken: function () {}, - revokeToken: function () {}, - saveToken: sinon.stub().returns(true), - }); - const handler = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model: model, - }); + describe('saveToken()', function () { + it('should call `model.saveToken()`', function () { + const client = {}; + const user = {}; + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + }); - sinon.stub(handler, "generateAccessToken").returns("foo"); - sinon.stub(handler, "generateRefreshToken").returns("bar"); - sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); - sinon.stub(handler, "getRefreshTokenExpiresAt").returns("baz"); + sinon.stub(handler, 'generateAccessToken').returns('foo'); + sinon.stub(handler, 'generateRefreshToken').returns('bar'); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - return handler - .saveToken(user, client, ["foobar"]) - .then(function () { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ - accessToken: "foo", - accessTokenExpiresAt: "biz", - refreshToken: "bar", - refreshTokenExpiresAt: "baz", - scope: ["foobar"], - }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); + return handler + .saveToken(user, client, ['foobar']) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + refreshToken: 'bar', + refreshTokenExpiresAt: 'baz', + scope: ['foobar'], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); - it("should call `model.saveToken()` without refresh token", function () { - const client = {}; - const user = {}; - const model = Model.from({ - getRefreshToken: function () {}, - revokeToken: function () {}, - saveToken: sinon.stub().returns(true), - }); - const handler = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model: model, - alwaysIssueNewRefreshToken: false, - }); + it('should call `model.saveToken()` without refresh token', function () { + const client = {}; + const user = {}; + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + alwaysIssueNewRefreshToken: false, + }); - sinon.stub(handler, "generateAccessToken").returns("foo"); - sinon.stub(handler, "generateRefreshToken").returns("bar"); - sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); - sinon.stub(handler, "getRefreshTokenExpiresAt").returns("baz"); + sinon.stub(handler, 'generateAccessToken').returns('foo'); + sinon.stub(handler, 'generateRefreshToken').returns('bar'); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - return handler - .saveToken(user, client, ["foobar"]) - .then(function () { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ - accessToken: "foo", - accessTokenExpiresAt: "biz", - scope: ["foobar"], - }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); + return handler + .saveToken(user, client, ['foobar']) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + scope: ['foobar'], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); - it("should call `model.saveToken()` with refresh token", function () { - const client = {}; - const user = {}; - const model = Model.from({ - getRefreshToken: function () {}, - revokeToken: function () {}, - saveToken: sinon.stub().returns(true), - }); - const handler = new RefreshTokenGrantType({ - accessTokenLifetime: 120, - model: model, - alwaysIssueNewRefreshToken: true, - }); + it('should call `model.saveToken()` with refresh token', function () { + const client = {}; + const user = {}; + const model = Model.from({ + getRefreshToken: function () {}, + revokeToken: function () {}, + saveToken: sinon.stub().returns(true), + }); + const handler = new RefreshTokenGrantType({ + accessTokenLifetime: 120, + model: model, + alwaysIssueNewRefreshToken: true, + }); - sinon.stub(handler, "generateAccessToken").returns("foo"); - sinon.stub(handler, "generateRefreshToken").returns("bar"); - sinon.stub(handler, "getAccessTokenExpiresAt").returns("biz"); - sinon.stub(handler, "getRefreshTokenExpiresAt").returns("baz"); + sinon.stub(handler, 'generateAccessToken').returns('foo'); + sinon.stub(handler, 'generateRefreshToken').returns('bar'); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns('biz'); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns('baz'); - return handler - .saveToken(user, client, ["foobar"]) - .then(function () { - model.saveToken.callCount.should.equal(1); - model.saveToken.firstCall.args.should.have.length(3); - model.saveToken.firstCall.args[0].should.eql({ - accessToken: "foo", - accessTokenExpiresAt: "biz", - refreshToken: "bar", - refreshTokenExpiresAt: "baz", - scope: ["foobar"], - }); - model.saveToken.firstCall.args[1].should.equal(client); - model.saveToken.firstCall.args[2].should.equal(user); - model.saveToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .saveToken(user, client, ['foobar']) + .then(function () { + model.saveToken.callCount.should.equal(1); + model.saveToken.firstCall.args.should.have.length(3); + model.saveToken.firstCall.args[0].should.eql({ + accessToken: 'foo', + accessTokenExpiresAt: 'biz', + refreshToken: 'bar', + refreshTokenExpiresAt: 'baz', + scope: ['foobar'], + }); + model.saveToken.firstCall.args[1].should.equal(client); + model.saveToken.firstCall.args[2].should.equal(user); + model.saveToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index 72a93d37..decf139a 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -1,197 +1,197 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AuthenticateHandler = require("../../../lib/handlers/authenticate-handler"); -const InvalidRequestError = require("../../../lib/errors/invalid-request-error"); -const Request = require("../../../lib/request"); -const Model = require("../../../lib/model"); -const sinon = require("sinon"); -const should = require("chai").should(); -const ServerError = require("../../../lib/errors/server-error"); +const AuthenticateHandler = require('../../../lib/handlers/authenticate-handler'); +const InvalidRequestError = require('../../../lib/errors/invalid-request-error'); +const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); +const sinon = require('sinon'); +const should = require('chai').should(); +const ServerError = require('../../../lib/errors/server-error'); /** * Test `AuthenticateHandler`. */ -describe("AuthenticateHandler", function () { - describe("getTokenFromRequest()", function () { - describe("with bearer token in the request authorization header", function () { - it("should throw an error if the token is malformed", () => { - const handler = new AuthenticateHandler({ - model: { getAccessToken() {} }, - }); - const request = new Request({ - body: {}, - headers: { - Authorization: "foo Bearer bar", - }, - method: "ANY", - query: {}, - }); - - try { - handler.getTokenFromRequestHeader(request); - - should.fail("should.fail", ""); - } catch (e) { - e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: malformed authorization header", - ); - } - }); - }); - - describe("with bearer token in the request authorization header", function () { - it("should call `getTokenFromRequestHeader()`", function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: {}, - headers: { Authorization: "Bearer foo" }, - method: {}, - query: {}, - }); - - sinon.stub(handler, "getTokenFromRequestHeader"); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestHeader.callCount.should.equal(1); - handler.getTokenFromRequestHeader.firstCall.args[0].should.equal( - request, - ); - handler.getTokenFromRequestHeader.restore(); - }); - }); - - describe("with bearer token in the request query", function () { - it("should call `getTokenFromRequestQuery()`", function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: { access_token: "foo" }, - }); - - sinon.stub(handler, "getTokenFromRequestQuery"); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestQuery.callCount.should.equal(1); - handler.getTokenFromRequestQuery.firstCall.args[0].should.equal( - request, - ); - handler.getTokenFromRequestQuery.restore(); - }); - }); - - describe("with bearer token in the request body", function () { - it("should call `getTokenFromRequestBody()`", function () { - const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, - }); - const request = new Request({ - body: { access_token: "foo" }, - headers: {}, - method: {}, - query: {}, - }); - - sinon.stub(handler, "getTokenFromRequestBody"); - - handler.getTokenFromRequest(request); - - handler.getTokenFromRequestBody.callCount.should.equal(1); - handler.getTokenFromRequestBody.firstCall.args[0].should.equal(request); - handler.getTokenFromRequestBody.restore(); - }); - }); - }); - - describe("getAccessToken()", function () { - it("should call `model.getAccessToken()`", function () { - const model = Model.from({ - getAccessToken: sinon.stub().returns({ user: {} }), - }); - const handler = new AuthenticateHandler({ model: model }); - - return handler - .getAccessToken("foo") - .then(function () { - model.getAccessToken.callCount.should.equal(1); - model.getAccessToken.firstCall.args.should.have.length(1); - model.getAccessToken.firstCall.args[0].should.equal("foo"); - model.getAccessToken.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); - - describe("validateAccessToken()", function () { - it("should fail if token has no valid `accessTokenExpiresAt` date", function () { - const model = Model.from({ - getAccessToken: function () {}, - }); - const handler = new AuthenticateHandler({ model: model }); - - let failed = false; - try { - handler.validateAccessToken({ - user: {}, - }); - } catch (err) { - err.should.be.an.instanceOf(ServerError); - failed = true; - } - failed.should.equal(true); - }); - - it("should succeed if token has valid `accessTokenExpiresAt` date", function () { - const model = Model.from({ - getAccessToken: function () {}, - }); - const handler = new AuthenticateHandler({ model: model }); - try { - handler.validateAccessToken({ - user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), - }); - } catch (err) { - should.fail(); - } - }); - }); - - describe("verifyScope()", function () { - it("should call `model.getAccessToken()` if scope is defined", function () { - const model = Model.from({ - getAccessToken: function () {}, - verifyScope: sinon.stub().returns(true), - }); - const handler = new AuthenticateHandler({ - addAcceptedScopesHeader: true, - addAuthorizedScopesHeader: true, - model: model, - scope: "bar", - }); - - return handler - .verifyScope(["foo"]) - .then(function () { - model.verifyScope.callCount.should.equal(1); - model.verifyScope.firstCall.args.should.have.length(2); - model.verifyScope.firstCall.args[0].should.eql(["foo"], ["bar"]); - model.verifyScope.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); +describe('AuthenticateHandler', function () { + describe('getTokenFromRequest()', function () { + describe('with bearer token in the request authorization header', function () { + it('should throw an error if the token is malformed', () => { + const handler = new AuthenticateHandler({ + model: { getAccessToken() {} }, + }); + const request = new Request({ + body: {}, + headers: { + Authorization: 'foo Bearer bar', + }, + method: 'ANY', + query: {}, + }); + + try { + handler.getTokenFromRequestHeader(request); + + should.fail('should.fail', ''); + } catch (e) { + e.should.be.an.instanceOf(InvalidRequestError); + e.message.should.equal( + 'Invalid request: malformed authorization header', + ); + } + }); + }); + + describe('with bearer token in the request authorization header', function () { + it('should call `getTokenFromRequestHeader()`', function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: { Authorization: 'Bearer foo' }, + method: {}, + query: {}, + }); + + sinon.stub(handler, 'getTokenFromRequestHeader'); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestHeader.callCount.should.equal(1); + handler.getTokenFromRequestHeader.firstCall.args[0].should.equal( + request, + ); + handler.getTokenFromRequestHeader.restore(); + }); + }); + + describe('with bearer token in the request query', function () { + it('should call `getTokenFromRequestQuery()`', function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: { access_token: 'foo' }, + }); + + sinon.stub(handler, 'getTokenFromRequestQuery'); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestQuery.callCount.should.equal(1); + handler.getTokenFromRequestQuery.firstCall.args[0].should.equal( + request, + ); + handler.getTokenFromRequestQuery.restore(); + }); + }); + + describe('with bearer token in the request body', function () { + it('should call `getTokenFromRequestBody()`', function () { + const handler = new AuthenticateHandler({ + model: { getAccessToken: function () {} }, + }); + const request = new Request({ + body: { access_token: 'foo' }, + headers: {}, + method: {}, + query: {}, + }); + + sinon.stub(handler, 'getTokenFromRequestBody'); + + handler.getTokenFromRequest(request); + + handler.getTokenFromRequestBody.callCount.should.equal(1); + handler.getTokenFromRequestBody.firstCall.args[0].should.equal(request); + handler.getTokenFromRequestBody.restore(); + }); + }); + }); + + describe('getAccessToken()', function () { + it('should call `model.getAccessToken()`', function () { + const model = Model.from({ + getAccessToken: sinon.stub().returns({ user: {} }), + }); + const handler = new AuthenticateHandler({ model: model }); + + return handler + .getAccessToken('foo') + .then(function () { + model.getAccessToken.callCount.should.equal(1); + model.getAccessToken.firstCall.args.should.have.length(1); + model.getAccessToken.firstCall.args[0].should.equal('foo'); + model.getAccessToken.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); + + describe('validateAccessToken()', function () { + it('should fail if token has no valid `accessTokenExpiresAt` date', function () { + const model = Model.from({ + getAccessToken: function () {}, + }); + const handler = new AuthenticateHandler({ model: model }); + + let failed = false; + try { + handler.validateAccessToken({ + user: {}, + }); + } catch (err) { + err.should.be.an.instanceOf(ServerError); + failed = true; + } + failed.should.equal(true); + }); + + it('should succeed if token has valid `accessTokenExpiresAt` date', function () { + const model = Model.from({ + getAccessToken: function () {}, + }); + const handler = new AuthenticateHandler({ model: model }); + try { + handler.validateAccessToken({ + user: {}, + accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + }); + } catch (err) { + should.fail(); + } + }); + }); + + describe('verifyScope()', function () { + it('should call `model.getAccessToken()` if scope is defined', function () { + const model = Model.from({ + getAccessToken: function () {}, + verifyScope: sinon.stub().returns(true), + }); + const handler = new AuthenticateHandler({ + addAcceptedScopesHeader: true, + addAuthorizedScopesHeader: true, + model: model, + scope: 'bar', + }); + + return handler + .verifyScope(['foo']) + .then(function () { + model.verifyScope.callCount.should.equal(1); + model.verifyScope.firstCall.args.should.have.length(2); + model.verifyScope.firstCall.args[0].should.eql(['foo'], ['bar']); + model.verifyScope.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 8df8af4d..d733a449 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -1,295 +1,295 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AuthorizeHandler = require("../../../lib/handlers/authorize-handler"); -const Request = require("../../../lib/request"); -const Response = require("../../../lib/response"); -const Model = require("../../../lib/model"); -const sinon = require("sinon"); -const should = require("chai").should(); +const AuthorizeHandler = require('../../../lib/handlers/authorize-handler'); +const Request = require('../../../lib/request'); +const Response = require('../../../lib/response'); +const Model = require('../../../lib/model'); +const sinon = require('sinon'); +const should = require('chai').should(); /** * Test `AuthorizeHandler`. */ -describe("AuthorizeHandler", function () { - describe("generateAuthorizationCode()", function () { - it("should call `model.generateAuthorizationCode()`", function () { - const model = Model.from({ - generateAuthorizationCode: sinon.stub().returns({}), - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model: model, - }); +describe('AuthorizeHandler', function () { + describe('generateAuthorizationCode()', function () { + it('should call `model.generateAuthorizationCode()`', function () { + const model = Model.from({ + generateAuthorizationCode: sinon.stub().returns({}), + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); - return handler - .generateAuthorizationCode() - .then(function () { - model.generateAuthorizationCode.callCount.should.equal(1); - model.generateAuthorizationCode.firstCall.thisValue.should.equal( - model, - ); - }) - .catch(should.fail); - }); - }); + return handler + .generateAuthorizationCode() + .then(function () { + model.generateAuthorizationCode.callCount.should.equal(1); + model.generateAuthorizationCode.firstCall.thisValue.should.equal( + model, + ); + }) + .catch(should.fail); + }); + }); - describe("getClient()", function () { - it("should call `model.getClient()`", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: sinon.stub().returns({ - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }), - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model: model, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret" }, - headers: {}, - method: {}, - query: {}, - }); + describe('getClient()', function () { + it('should call `model.getClient()`', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: sinon.stub().returns({ + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }), + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: {}, + query: {}, + }); - return handler - .getClient(request) - .then(function () { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .getClient(request) + .then(function () { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("getUser()", function () { - it("should call `authenticateHandler.getUser()`", function () { - const authenticateHandler = { - handle: sinon.stub().returns(Promise.resolve({})), - }; - const model = Model.from({ - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const handler = new AuthorizeHandler({ - authenticateHandler: authenticateHandler, - authorizationCodeLifetime: 120, - model: model, - }); - const request = new Request({ - body: {}, - headers: {}, - method: {}, - query: {}, - }); - const response = new Response(); + describe('getUser()', function () { + it('should call `authenticateHandler.getUser()`', function () { + const authenticateHandler = { + handle: sinon.stub().returns(Promise.resolve({})), + }; + const model = Model.from({ + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const handler = new AuthorizeHandler({ + authenticateHandler: authenticateHandler, + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: {}, + headers: {}, + method: {}, + query: {}, + }); + const response = new Response(); - return handler - .getUser(request, response) - .then(function () { - authenticateHandler.handle.callCount.should.equal(1); - authenticateHandler.handle.firstCall.args.should.have.length(2); - authenticateHandler.handle.firstCall.args[0].should.equal(request); - authenticateHandler.handle.firstCall.args[1].should.equal(response); - }) - .catch(should.fail); - }); - }); + return handler + .getUser(request, response) + .then(function () { + authenticateHandler.handle.callCount.should.equal(1); + authenticateHandler.handle.firstCall.args.should.have.length(2); + authenticateHandler.handle.firstCall.args[0].should.equal(request); + authenticateHandler.handle.firstCall.args[1].should.equal(response); + }) + .catch(should.fail); + }); + }); - describe("saveAuthorizationCode()", function () { - it("should call `model.saveAuthorizationCode()`", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: sinon.stub().returns({}), - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model: model, - }); + describe('saveAuthorizationCode()', function () { + it('should call `model.saveAuthorizationCode()`', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: sinon.stub().returns({}), + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); - return handler - .saveAuthorizationCode("foo", "bar", ["qux"], "biz", "baz", "boz") - .then(function () { - model.saveAuthorizationCode.callCount.should.equal(1); - model.saveAuthorizationCode.firstCall.args.should.have.length(3); - model.saveAuthorizationCode.firstCall.args[0].should.eql({ - authorizationCode: "foo", - expiresAt: "bar", - redirectUri: "baz", - scope: ["qux"], - }); - model.saveAuthorizationCode.firstCall.args[1].should.equal("biz"); - model.saveAuthorizationCode.firstCall.args[2].should.equal("boz"); - model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); + return handler + .saveAuthorizationCode('foo', 'bar', ['qux'], 'biz', 'baz', 'boz') + .then(function () { + model.saveAuthorizationCode.callCount.should.equal(1); + model.saveAuthorizationCode.firstCall.args.should.have.length(3); + model.saveAuthorizationCode.firstCall.args[0].should.eql({ + authorizationCode: 'foo', + expiresAt: 'bar', + redirectUri: 'baz', + scope: ['qux'], + }); + model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); + model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); + model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); - it("should call `model.saveAuthorizationCode()` with code challenge", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: sinon.stub().returns({}), - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model: model, - }); + it('should call `model.saveAuthorizationCode()` with code challenge', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: sinon.stub().returns({}), + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); - return handler - .saveAuthorizationCode( - "foo", - "bar", - ["qux"], - "biz", - "baz", - "boz", - "codeChallenge", - "codeChallengeMethod", - ) - .then(function () { - model.saveAuthorizationCode.callCount.should.equal(1); - model.saveAuthorizationCode.firstCall.args.should.have.length(3); - model.saveAuthorizationCode.firstCall.args[0].should.eql({ - authorizationCode: "foo", - expiresAt: "bar", - redirectUri: "baz", - scope: ["qux"], - codeChallenge: "codeChallenge", - codeChallengeMethod: "codeChallengeMethod", - }); - model.saveAuthorizationCode.firstCall.args[1].should.equal("biz"); - model.saveAuthorizationCode.firstCall.args[2].should.equal("boz"); - model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .saveAuthorizationCode( + 'foo', + 'bar', + ['qux'], + 'biz', + 'baz', + 'boz', + 'codeChallenge', + 'codeChallengeMethod', + ) + .then(function () { + model.saveAuthorizationCode.callCount.should.equal(1); + model.saveAuthorizationCode.firstCall.args.should.have.length(3); + model.saveAuthorizationCode.firstCall.args[0].should.eql({ + authorizationCode: 'foo', + expiresAt: 'bar', + redirectUri: 'baz', + scope: ['qux'], + codeChallenge: 'codeChallenge', + codeChallengeMethod: 'codeChallengeMethod', + }); + model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); + model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); + model.saveAuthorizationCode.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); - describe("validateRedirectUri()", function () { - it("should call `model.validateRedirectUri()`", function () { - const client = { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const redirect_uri = "http://example.com/cb/2"; - const model = Model.from({ - getAccessToken: function () {}, - getClient: sinon.stub().returns(client), - saveAuthorizationCode: function () {}, - validateRedirectUri: sinon.stub().returns(true), - }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model: model, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret", redirect_uri }, - headers: {}, - method: {}, - query: {}, - }); + describe('validateRedirectUri()', function () { + it('should call `model.validateRedirectUri()`', function () { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const redirect_uri = 'http://example.com/cb/2'; + const model = Model.from({ + getAccessToken: function () {}, + getClient: sinon.stub().returns(client), + saveAuthorizationCode: function () {}, + validateRedirectUri: sinon.stub().returns(true), + }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret', redirect_uri }, + headers: {}, + method: {}, + query: {}, + }); - return handler - .getClient(request) - .then(function () { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.thisValue.should.equal(model); + return handler + .getClient(request) + .then(function () { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.thisValue.should.equal(model); - model.validateRedirectUri.callCount.should.equal(1); - model.validateRedirectUri.firstCall.args.should.have.length(2); - model.validateRedirectUri.firstCall.args[0].should.equal( - redirect_uri, - ); - model.validateRedirectUri.firstCall.args[1].should.equal(client); - model.validateRedirectUri.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); + model.validateRedirectUri.callCount.should.equal(1); + model.validateRedirectUri.firstCall.args.should.have.length(2); + model.validateRedirectUri.firstCall.args[0].should.equal( + redirect_uri, + ); + model.validateRedirectUri.firstCall.args[1].should.equal(client); + model.validateRedirectUri.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); - it("should be successful validation", function () { - const client = { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const redirect_uri = "http://example.com/cb"; - const model = Model.from({ - getAccessToken: function () {}, - getClient: sinon.stub().returns(client), - saveAuthorizationCode: function () {}, - validateRedirectUri: function (redirectUri, client) { - return client.redirectUris.includes(redirectUri); - }, - }); + it('should be successful validation', function () { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const redirect_uri = 'http://example.com/cb'; + const model = Model.from({ + getAccessToken: function () {}, + getClient: sinon.stub().returns(client), + saveAuthorizationCode: function () {}, + validateRedirectUri: function (redirectUri, client) { + return client.redirectUris.includes(redirectUri); + }, + }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model: model, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret", redirect_uri }, - headers: {}, - method: {}, - query: {}, - }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret', redirect_uri }, + headers: {}, + method: {}, + query: {}, + }); - return handler.getClient(request).then((client) => { - client.should.equal(client); - }); - }); + return handler.getClient(request).then((client) => { + client.should.equal(client); + }); + }); - it("should be unsuccessful validation", function () { - const client = { - grants: ["authorization_code"], - redirectUris: ["http://example.com/cb"], - }; - const redirect_uri = "http://example.com/callback"; - const model = Model.from({ - getAccessToken: function () {}, - getClient: sinon.stub().returns(client), - saveAuthorizationCode: function () {}, - validateRedirectUri: function (redirectUri, client) { - return client.redirectUris.includes(redirectUri); - }, - }); + it('should be unsuccessful validation', function () { + const client = { + grants: ['authorization_code'], + redirectUris: ['http://example.com/cb'], + }; + const redirect_uri = 'http://example.com/callback'; + const model = Model.from({ + getAccessToken: function () {}, + getClient: sinon.stub().returns(client), + saveAuthorizationCode: function () {}, + validateRedirectUri: function (redirectUri, client) { + return client.redirectUris.includes(redirectUri); + }, + }); - const handler = new AuthorizeHandler({ - authorizationCodeLifetime: 120, - model: model, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret", redirect_uri }, - headers: {}, - method: {}, - query: {}, - }); + const handler = new AuthorizeHandler({ + authorizationCodeLifetime: 120, + model: model, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret', redirect_uri }, + headers: {}, + method: {}, + query: {}, + }); - return handler - .getClient(request) - .then(() => { - throw Error("should not resolve"); - }) - .catch((err) => { - err.name.should.equal("invalid_client"); - err.message.should.equal( - "Invalid client: `redirect_uri` does not match client value", - ); - }); - }); - }); + return handler + .getClient(request) + .then(() => { + throw Error('should not resolve'); + }) + .catch((err) => { + err.name.should.equal('invalid_client'); + err.message.should.equal( + 'Invalid client: `redirect_uri` does not match client value', + ); + }); + }); + }); }); diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js index 269ebc13..89b30fb2 100644 --- a/test/unit/handlers/token-handler_test.js +++ b/test/unit/handlers/token-handler_test.js @@ -1,48 +1,48 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const Request = require("../../../lib/request"); -const Model = require("../../../lib/model"); -const TokenHandler = require("../../../lib/handlers/token-handler"); -const sinon = require("sinon"); -const should = require("chai").should(); +const Request = require('../../../lib/request'); +const Model = require('../../../lib/model'); +const TokenHandler = require('../../../lib/handlers/token-handler'); +const sinon = require('sinon'); +const should = require('chai').should(); /** * Test `TokenHandler`. */ -describe("TokenHandler", function () { - describe("getClient()", function () { - it("should call `model.getClient()`", function () { - const model = Model.from({ - getClient: sinon.stub().returns({ grants: ["password"] }), - saveToken: function () {}, - }); - const handler = new TokenHandler({ - accessTokenLifetime: 120, - model: model, - refreshTokenLifetime: 120, - }); - const request = new Request({ - body: { client_id: 12345, client_secret: "secret" }, - headers: {}, - method: {}, - query: {}, - }); +describe('TokenHandler', function () { + describe('getClient()', function () { + it('should call `model.getClient()`', function () { + const model = Model.from({ + getClient: sinon.stub().returns({ grants: ['password'] }), + saveToken: function () {}, + }); + const handler = new TokenHandler({ + accessTokenLifetime: 120, + model: model, + refreshTokenLifetime: 120, + }); + const request = new Request({ + body: { client_id: 12345, client_secret: 'secret' }, + headers: {}, + method: {}, + query: {}, + }); - return handler - .getClient(request) - .then(function () { - model.getClient.callCount.should.equal(1); - model.getClient.firstCall.args.should.have.length(2); - model.getClient.firstCall.args[0].should.equal(12345); - model.getClient.firstCall.args[1].should.equal("secret"); - model.getClient.firstCall.thisValue.should.equal(model); - }) - .catch(should.fail); - }); - }); + return handler + .getClient(request) + .then(function () { + model.getClient.callCount.should.equal(1); + model.getClient.firstCall.args.should.have.length(2); + model.getClient.firstCall.args[0].should.equal(12345); + model.getClient.firstCall.args[1].should.equal('secret'); + model.getClient.firstCall.thisValue.should.equal(model); + }) + .catch(should.fail); + }); + }); }); diff --git a/test/unit/models/model_wrapper_test.js b/test/unit/models/model_wrapper_test.js index ddca59dd..147c2215 100644 --- a/test/unit/models/model_wrapper_test.js +++ b/test/unit/models/model_wrapper_test.js @@ -1,32 +1,32 @@ -const Model = require("../../../lib/model"); -const { expect } = require("chai"); +const Model = require('../../../lib/model'); +const { expect } = require('chai'); -describe("ModelWrapper", () => { - const expectThrows = async (fn) => { - try { - await fn(); - expect.fail(); - } catch (e) { - expect(e.message).to.include("not implemented"); - } - }; - it("throws on all functions when used via constructor", async () => { - const m = new Model(); - await expectThrows(() => m.generateAccessToken()); - await expectThrows(() => m.generateAuthorizationCode()); - await expectThrows(() => m.generateRefreshToken()); - await expectThrows(() => m.getAccessToken()); - await expectThrows(() => m.getAuthorizationCode()); - await expectThrows(() => m.getClient()); - await expectThrows(() => m.getRefreshToken()); - await expectThrows(() => m.getUser()); - await expectThrows(() => m.getUserFromClient()); - await expectThrows(() => m.revokeAuthorizationCode()); - await expectThrows(() => m.revokeToken()); - await expectThrows(() => m.saveAuthorizationCode()); - await expectThrows(() => m.saveToken()); - await expectThrows(() => m.verifyScope()); - await expectThrows(() => m.validateRedirectUri()); - await expectThrows(() => m.validateScope()); - }); +describe('ModelWrapper', () => { + const expectThrows = async (fn) => { + try { + await fn(); + expect.fail(); + } catch (e) { + expect(e.message).to.include('not implemented'); + } + }; + it('throws on all functions when used via constructor', async () => { + const m = new Model(); + await expectThrows(() => m.generateAccessToken()); + await expectThrows(() => m.generateAuthorizationCode()); + await expectThrows(() => m.generateRefreshToken()); + await expectThrows(() => m.getAccessToken()); + await expectThrows(() => m.getAuthorizationCode()); + await expectThrows(() => m.getClient()); + await expectThrows(() => m.getRefreshToken()); + await expectThrows(() => m.getUser()); + await expectThrows(() => m.getUserFromClient()); + await expectThrows(() => m.revokeAuthorizationCode()); + await expectThrows(() => m.revokeToken()); + await expectThrows(() => m.saveAuthorizationCode()); + await expectThrows(() => m.saveToken()); + await expectThrows(() => m.verifyScope()); + await expectThrows(() => m.validateRedirectUri()); + await expectThrows(() => m.validateScope()); + }); }); diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model_test.js index 6eed7f8f..2c1f56bd 100644 --- a/test/unit/models/token-model_test.js +++ b/test/unit/models/token-model_test.js @@ -1,160 +1,160 @@ -const TokenModel = require("../../../lib/models/token-model"); -const InvalidArgumentError = require("../../../lib/errors/invalid-argument-error"); -const should = require("chai").should(); +const TokenModel = require('../../../lib/models/token-model'); +const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error'); +const should = require('chai').should(); /** * Test `Server`. */ -describe("TokenModel", function () { - describe("constructor()", function () { - it("throws, if data is empty", function () { - try { - new TokenModel(); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `accessToken`"); - } - }); - it("throws, if `accessToken` is missing", function () { - const atExpiresAt = new Date(); - atExpiresAt.setHours(new Date().getHours() + 1); - - const data = { - client: "bar", - user: "tar", - accessTokenExpiresAt: atExpiresAt, - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `accessToken`"); - } - }); - it("throws, if `client` is missing", function () { - const atExpiresAt = new Date(); - atExpiresAt.setHours(new Date().getHours() + 1); - - const data = { - accessToken: "foo", - user: "tar", - accessTokenExpiresAt: atExpiresAt, - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `client`"); - } - }); - it("throws, if `user` is missing", function () { - const atExpiresAt = new Date(); - atExpiresAt.setHours(new Date().getHours() + 1); - - const data = { - accessToken: "foo", - client: "bar", - accessTokenExpiresAt: atExpiresAt, - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Missing parameter: `user`"); - } - }); - it("throws, if `accessTokenExpiresAt` is not a Date", function () { - const data = { - accessToken: "foo", - client: "bar", - user: "tar", - accessTokenExpiresAt: "11/10/2023", - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Invalid parameter: `accessTokenExpiresAt`"); - } - }); - it("throws, if `refreshTokenExpiresAt` is not a Date", function () { - const data = { - accessToken: "foo", - client: "bar", - user: "tar", - refreshTokenExpiresAt: "11/10/2023", - }; - - try { - new TokenModel(data); - should.fail(); - } catch (e) { - e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal("Invalid parameter: `refreshTokenExpiresAt`"); - } - }); - it("should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set", function () { - const atExpiresAt = new Date(); - atExpiresAt.setHours(new Date().getHours() + 1); - - const data = { - accessToken: "foo", - client: "bar", - user: "tar", - accessTokenExpiresAt: atExpiresAt, - }; - - const model = new TokenModel(data); - should.exist(model.accessTokenLifetime); - model.accessTokenLifetime.should.a("number"); - model.accessTokenLifetime.should.be.approximately(3600, 2); - }); - - it("should throw if the required arguments are not provided", () => { - should.throw(() => { - new TokenModel({}); - }); - }); - - it("should ignore custom attributes if allowExtendedTokenAttributes is not specified as true", () => { - const model = new TokenModel({ - accessToken: "token", - client: "client", - user: "user", - myCustomAttribute: "myCustomValue", - }); - - should.not.exist(model["myCustomAttribute"]); - should.not.exist(model["customAttributes"]); - }); - - it("should set custom attributes on the customAttributes field if allowExtendedTokenAttributes is specified as true", () => { - const model = new TokenModel( - { - accessToken: "token", - client: "client", - user: "user", - myCustomAttribute: "myCustomValue", - }, - { - allowExtendedTokenAttributes: true, - }, - ); - - should.not.exist(model["myCustomAttribute"]); - model["customAttributes"].should.be.an("object"); - model["customAttributes"]["myCustomAttribute"].should.equal( - "myCustomValue", - ); - }); - }); +describe('TokenModel', function () { + describe('constructor()', function () { + it('throws, if data is empty', function () { + try { + new TokenModel(); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessToken`'); + } + }); + it('throws, if `accessToken` is missing', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + client: 'bar', + user: 'tar', + accessTokenExpiresAt: atExpiresAt, + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `accessToken`'); + } + }); + it('throws, if `client` is missing', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: 'foo', + user: 'tar', + accessTokenExpiresAt: atExpiresAt, + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `client`'); + } + }); + it('throws, if `user` is missing', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: 'foo', + client: 'bar', + accessTokenExpiresAt: atExpiresAt, + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Missing parameter: `user`'); + } + }); + it('throws, if `accessTokenExpiresAt` is not a Date', function () { + const data = { + accessToken: 'foo', + client: 'bar', + user: 'tar', + accessTokenExpiresAt: '11/10/2023', + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `accessTokenExpiresAt`'); + } + }); + it('throws, if `refreshTokenExpiresAt` is not a Date', function () { + const data = { + accessToken: 'foo', + client: 'bar', + user: 'tar', + refreshTokenExpiresAt: '11/10/2023', + }; + + try { + new TokenModel(data); + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidArgumentError); + e.message.should.equal('Invalid parameter: `refreshTokenExpiresAt`'); + } + }); + it('should calculate `accessTokenLifetime` if `accessTokenExpiresAt` is set', function () { + const atExpiresAt = new Date(); + atExpiresAt.setHours(new Date().getHours() + 1); + + const data = { + accessToken: 'foo', + client: 'bar', + user: 'tar', + accessTokenExpiresAt: atExpiresAt, + }; + + const model = new TokenModel(data); + should.exist(model.accessTokenLifetime); + model.accessTokenLifetime.should.a('number'); + model.accessTokenLifetime.should.be.approximately(3600, 2); + }); + + it('should throw if the required arguments are not provided', () => { + should.throw(() => { + new TokenModel({}); + }); + }); + + it('should ignore custom attributes if allowExtendedTokenAttributes is not specified as true', () => { + const model = new TokenModel({ + accessToken: 'token', + client: 'client', + user: 'user', + myCustomAttribute: 'myCustomValue', + }); + + should.not.exist(model['myCustomAttribute']); + should.not.exist(model['customAttributes']); + }); + + it('should set custom attributes on the customAttributes field if allowExtendedTokenAttributes is specified as true', () => { + const model = new TokenModel( + { + accessToken: 'token', + client: 'client', + user: 'user', + myCustomAttribute: 'myCustomValue', + }, + { + allowExtendedTokenAttributes: true, + }, + ); + + should.not.exist(model['myCustomAttribute']); + model['customAttributes'].should.be.an('object'); + model['customAttributes']['myCustomAttribute'].should.equal( + 'myCustomValue', + ); + }); + }); }); diff --git a/test/unit/pkce/pkce_test.js b/test/unit/pkce/pkce_test.js index c5744cae..4ee90b30 100644 --- a/test/unit/pkce/pkce_test.js +++ b/test/unit/pkce/pkce_test.js @@ -1,121 +1,121 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const pkce = require("../../../lib/pkce/pkce"); -const should = require("chai").should(); -const { base64URLEncode } = require("../../../lib/utils/string-util"); -const { createHash } = require("../../../lib/utils/crypto-util"); +const pkce = require('../../../lib/pkce/pkce'); +const should = require('chai').should(); +const { base64URLEncode } = require('../../../lib/utils/string-util'); +const { createHash } = require('../../../lib/utils/crypto-util'); -describe("PKCE", function () { - describe(pkce.isPKCERequest.name, function () { - it("returns, whether parameters define a PKCE request", function () { - [ - [true, "authorization_code", "foo"], - [ - true, - "authorization_code", - "123123123123123123123123123123123123123123123", - ], - [false, "authorization_code", ""], - [false, "authorization_code", undefined], - [false, "foo_code", "123123123123123123123123123123123123123123123"], - [false, "", "123123123123123123123123123123123123123123123"], - [false, undefined, "123123123123123123123123123123123123123123123"], - [false, "foo_code", "bar"], - ].forEach((triple) => { - should.equal( - triple[0], - pkce.isPKCERequest({ - grantType: triple[1], - codeVerifier: triple[2], - }), - ); - }); - }); - }); - describe(pkce.codeChallengeMatchesABNF.name, function () { - it("returns whether a string matches the criteria for codeChallenge", function () { - [ - [false, undefined], - [false, null], - [false, ""], - [false, "123123123112312312311231231231123123123112"], // too short - [false, "123123123112312312311231231231123123123112+"], // invalid chars - [ - false, - "123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123", - ], // too long - // invalid chars - [ - true, - "-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ", - ], - ].forEach((pair) => { - should.equal(pair[0], pkce.codeChallengeMatchesABNF(pair[1])); - }); - }); - }); - describe(pkce.getHashForCodeChallenge.name, function () { - it("returns nothing if method is not valid", function () { - const verifier = - "-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ"; +describe('PKCE', function () { + describe(pkce.isPKCERequest.name, function () { + it('returns, whether parameters define a PKCE request', function () { + [ + [true, 'authorization_code', 'foo'], + [ + true, + 'authorization_code', + '123123123123123123123123123123123123123123123', + ], + [false, 'authorization_code', ''], + [false, 'authorization_code', undefined], + [false, 'foo_code', '123123123123123123123123123123123123123123123'], + [false, '', '123123123123123123123123123123123123123123123'], + [false, undefined, '123123123123123123123123123123123123123123123'], + [false, 'foo_code', 'bar'], + ].forEach((triple) => { + should.equal( + triple[0], + pkce.isPKCERequest({ + grantType: triple[1], + codeVerifier: triple[2], + }), + ); + }); + }); + }); + describe(pkce.codeChallengeMatchesABNF.name, function () { + it('returns whether a string matches the criteria for codeChallenge', function () { + [ + [false, undefined], + [false, null], + [false, ''], + [false, '123123123112312312311231231231123123123112'], // too short + [false, '123123123112312312311231231231123123123112+'], // invalid chars + [ + false, + '123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123', + ], // too long + // invalid chars + [ + true, + '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ', + ], + ].forEach((pair) => { + should.equal(pair[0], pkce.codeChallengeMatchesABNF(pair[1])); + }); + }); + }); + describe(pkce.getHashForCodeChallenge.name, function () { + it('returns nothing if method is not valid', function () { + const verifier = + '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; - [ - [undefined, undefined, verifier], - [undefined, null, verifier], - [undefined, "", verifier], - [undefined, "foo", verifier], - ].forEach((triple) => { - should.equal( - triple[0], - pkce.getHashForCodeChallenge({ - method: triple[1], - verifier: triple[2], - }), - ); - }); - }); - it("return the verifier on plain and undefined on S256 if verifier is falsy", function () { - [ - [undefined, "plain", undefined], - [undefined, "S256", undefined], - [undefined, "plain", ""], - [undefined, "S256", ""], - [undefined, "plain", null], - [undefined, "S256", null], - ].forEach((triple) => { - should.equal( - triple[0], - pkce.getHashForCodeChallenge({ - method: triple[1], - verifier: triple[2], - }), - ); - }); - }); - it("returns the unhashed verifier when method is plain", function () { - const verifier = - "-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ"; - const hash = pkce.getHashForCodeChallenge({ method: "plain", verifier }); - should.equal(hash, verifier); - }); - it("returns the hash verifier when method is S256", function () { - const verifier = - "-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ"; - const hash = pkce.getHashForCodeChallenge({ method: "S256", verifier }); - const expectedHash = base64URLEncode(createHash({ data: verifier })); - should.equal(hash, expectedHash); - }); - }); - describe(pkce.isValidMethod.name, function () { - it("returns if a method is plain or S256", function () { - should.equal(pkce.isValidMethod("plain"), true); - should.equal(pkce.isValidMethod("S256"), true); - should.equal(pkce.isValidMethod("foo"), false); - should.equal(pkce.isValidMethod(), false); - }); - }); + [ + [undefined, undefined, verifier], + [undefined, null, verifier], + [undefined, '', verifier], + [undefined, 'foo', verifier], + ].forEach((triple) => { + should.equal( + triple[0], + pkce.getHashForCodeChallenge({ + method: triple[1], + verifier: triple[2], + }), + ); + }); + }); + it('return the verifier on plain and undefined on S256 if verifier is falsy', function () { + [ + [undefined, 'plain', undefined], + [undefined, 'S256', undefined], + [undefined, 'plain', ''], + [undefined, 'S256', ''], + [undefined, 'plain', null], + [undefined, 'S256', null], + ].forEach((triple) => { + should.equal( + triple[0], + pkce.getHashForCodeChallenge({ + method: triple[1], + verifier: triple[2], + }), + ); + }); + }); + it('returns the unhashed verifier when method is plain', function () { + const verifier = + '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; + const hash = pkce.getHashForCodeChallenge({ method: 'plain', verifier }); + should.equal(hash, verifier); + }); + it('returns the hash verifier when method is S256', function () { + const verifier = + '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; + const hash = pkce.getHashForCodeChallenge({ method: 'S256', verifier }); + const expectedHash = base64URLEncode(createHash({ data: verifier })); + should.equal(hash, expectedHash); + }); + }); + describe(pkce.isValidMethod.name, function () { + it('returns if a method is plain or S256', function () { + should.equal(pkce.isValidMethod('plain'), true); + should.equal(pkce.isValidMethod('S256'), true); + should.equal(pkce.isValidMethod('foo'), false); + should.equal(pkce.isValidMethod(), false); + }); + }); }); diff --git a/test/unit/request_test.js b/test/unit/request_test.js index 73b4bce6..ed121b02 100644 --- a/test/unit/request_test.js +++ b/test/unit/request_test.js @@ -1,215 +1,215 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const Request = require("../../lib/request"); -const InvalidArgumentError = require("../../lib/errors/invalid-argument-error"); -const should = require("chai").should(); +const Request = require('../../lib/request'); +const InvalidArgumentError = require('../../lib/errors/invalid-argument-error'); +const should = require('chai').should(); /** * Test `Request`. */ function generateBaseRequest() { - return { - query: { - foo: "bar", - }, - method: "GET", - headers: { - bar: "foo", - }, - body: { - foobar: "barfoo", - }, - }; + return { + query: { + foo: 'bar', + }, + method: 'GET', + headers: { + bar: 'foo', + }, + body: { + foobar: 'barfoo', + }, + }; } -describe("Request", function () { - it("should throw on missing args", function () { - const args = [ - [undefined, InvalidArgumentError, "Missing parameter: `headers`"], - [null, TypeError, "Cannot destructure property 'headers'"], - [{}, InvalidArgumentError, "Missing parameter: `headers`"], - [{ headers: {} }, InvalidArgumentError, "Missing parameter: `method`"], - [ - { headers: {}, method: "GET" }, - InvalidArgumentError, - "Missing parameter: `query`", - ], - ]; - - args.forEach(([value, error, message]) => { - try { - new Request(value); - } catch (e) { - e.should.be.instanceOf(error); - e.message.should.include(message); - } - }); - }); - it("should instantiate with a basic request", function () { - const originalRequest = generateBaseRequest(); - - const request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - }); - - it("should allow a request to be passed without a body", function () { - const originalRequest = generateBaseRequest(); - delete originalRequest.body; - - const request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql({}); - }); - - it("should throw if headers are not passed to the constructor", function () { - const originalRequest = generateBaseRequest(); - delete originalRequest.headers; - - (function () { - new Request(originalRequest); - }).should.throw("Missing parameter: `headers`"); - }); - - it("should throw if query string isn't passed to the constructor", function () { - const originalRequest = generateBaseRequest(); - delete originalRequest.query; - - (function () { - new Request(originalRequest); - }).should.throw("Missing parameter: `query`"); - }); - - it("should throw if method isn't passed to the constructor", function () { - const originalRequest = generateBaseRequest(); - delete originalRequest.method; - - (function () { - new Request(originalRequest); - }).should.throw("Missing parameter: `method`"); - }); - - it("should convert all header keys to lowercase", function () { - const originalRequest = generateBaseRequest(); - originalRequest.headers = { - Foo: "bar", - BAR: "foo", - }; - - const request = new Request(originalRequest); - request.headers.foo.should.eql("bar"); - request.headers.bar.should.eql("foo"); - should.not.exist(request.headers.Foo); - should.not.exist(request.headers.BAR); - }); - - it("should include additional properties passed in the request", function () { - const originalRequest = generateBaseRequest(); - originalRequest.custom = { - newFoo: "newBar", - }; - - originalRequest.custom2 = { - newBar: "newFoo", - }; - - const request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - request.custom.should.eql(originalRequest.custom); - request.custom2.should.eql(originalRequest.custom2); - }); - - it("should include additional properties passed in the request", function () { - const originalRequest = generateBaseRequest(); - originalRequest.custom = { - newFoo: "newBar", - }; - - originalRequest.custom2 = { - newBar: "newFoo", - }; - - const request = new Request(originalRequest); - request.headers.should.eql(originalRequest.headers); - request.method.should.eql(originalRequest.method); - request.query.should.eql(originalRequest.query); - request.body.should.eql(originalRequest.body); - request.custom.should.eql(originalRequest.custom); - request.custom2.should.eql(originalRequest.custom2); - }); - - it("should not allow overwriting methods on the Request prototype via custom properties", () => { - const request = new Request({ - query: {}, - method: "GET", - headers: { - "content-type": "application/json", - }, - get() { - // malicious attempt to override the 'get' method - return "text/html"; - }, - }); - - request.get("content-type").should.equal("application/json"); - }); - - it("should allow getting of headers using `request.get`", function () { - const originalRequest = generateBaseRequest(); - - const request = new Request(originalRequest); - request.get("bar").should.eql(originalRequest.headers.bar); - }); - - it("should allow getting of headers using `request.get`", function () { - const originalRequest = generateBaseRequest(); - - const request = new Request(originalRequest); - request.get("bar").should.eql(originalRequest.headers.bar); - }); - - it("should allow getting of headers using `request.get`", function () { - const originalRequest = generateBaseRequest(); - - const request = new Request(originalRequest); - request.get("bar").should.eql(originalRequest.headers.bar); - }); - - it("should validate the content-type", function () { - const originalRequest = generateBaseRequest(); - originalRequest.headers["content-type"] = - "application/x-www-form-urlencoded"; - originalRequest.headers["content-length"] = JSON.stringify( - originalRequest.body, - ).length; - - const request = new Request(originalRequest); - request - .is("application/x-www-form-urlencoded") - .should.eql("application/x-www-form-urlencoded"); - }); - - it("should return false if the content-type is invalid", function () { - const originalRequest = generateBaseRequest(); - originalRequest.headers["content-type"] = - "application/x-www-form-urlencoded"; - originalRequest.headers["content-length"] = JSON.stringify( - originalRequest.body, - ).length; - - const request = new Request(originalRequest); - request.is("application/json").should.eql(false); - }); +describe('Request', function () { + it('should throw on missing args', function () { + const args = [ + [undefined, InvalidArgumentError, 'Missing parameter: `headers`'], + [null, TypeError, "Cannot destructure property 'headers'"], + [{}, InvalidArgumentError, 'Missing parameter: `headers`'], + [{ headers: {} }, InvalidArgumentError, 'Missing parameter: `method`'], + [ + { headers: {}, method: 'GET' }, + InvalidArgumentError, + 'Missing parameter: `query`', + ], + ]; + + args.forEach(([value, error, message]) => { + try { + new Request(value); + } catch (e) { + e.should.be.instanceOf(error); + e.message.should.include(message); + } + }); + }); + it('should instantiate with a basic request', function () { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + }); + + it('should allow a request to be passed without a body', function () { + const originalRequest = generateBaseRequest(); + delete originalRequest.body; + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql({}); + }); + + it('should throw if headers are not passed to the constructor', function () { + const originalRequest = generateBaseRequest(); + delete originalRequest.headers; + + (function () { + new Request(originalRequest); + }).should.throw('Missing parameter: `headers`'); + }); + + it("should throw if query string isn't passed to the constructor", function () { + const originalRequest = generateBaseRequest(); + delete originalRequest.query; + + (function () { + new Request(originalRequest); + }).should.throw('Missing parameter: `query`'); + }); + + it("should throw if method isn't passed to the constructor", function () { + const originalRequest = generateBaseRequest(); + delete originalRequest.method; + + (function () { + new Request(originalRequest); + }).should.throw('Missing parameter: `method`'); + }); + + it('should convert all header keys to lowercase', function () { + const originalRequest = generateBaseRequest(); + originalRequest.headers = { + Foo: 'bar', + BAR: 'foo', + }; + + const request = new Request(originalRequest); + request.headers.foo.should.eql('bar'); + request.headers.bar.should.eql('foo'); + should.not.exist(request.headers.Foo); + should.not.exist(request.headers.BAR); + }); + + it('should include additional properties passed in the request', function () { + const originalRequest = generateBaseRequest(); + originalRequest.custom = { + newFoo: 'newBar', + }; + + originalRequest.custom2 = { + newBar: 'newFoo', + }; + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + request.custom.should.eql(originalRequest.custom); + request.custom2.should.eql(originalRequest.custom2); + }); + + it('should include additional properties passed in the request', function () { + const originalRequest = generateBaseRequest(); + originalRequest.custom = { + newFoo: 'newBar', + }; + + originalRequest.custom2 = { + newBar: 'newFoo', + }; + + const request = new Request(originalRequest); + request.headers.should.eql(originalRequest.headers); + request.method.should.eql(originalRequest.method); + request.query.should.eql(originalRequest.query); + request.body.should.eql(originalRequest.body); + request.custom.should.eql(originalRequest.custom); + request.custom2.should.eql(originalRequest.custom2); + }); + + it('should not allow overwriting methods on the Request prototype via custom properties', () => { + const request = new Request({ + query: {}, + method: 'GET', + headers: { + 'content-type': 'application/json', + }, + get() { + // malicious attempt to override the 'get' method + return 'text/html'; + }, + }); + + request.get('content-type').should.equal('application/json'); + }); + + it('should allow getting of headers using `request.get`', function () { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get('bar').should.eql(originalRequest.headers.bar); + }); + + it('should allow getting of headers using `request.get`', function () { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get('bar').should.eql(originalRequest.headers.bar); + }); + + it('should allow getting of headers using `request.get`', function () { + const originalRequest = generateBaseRequest(); + + const request = new Request(originalRequest); + request.get('bar').should.eql(originalRequest.headers.bar); + }); + + it('should validate the content-type', function () { + const originalRequest = generateBaseRequest(); + originalRequest.headers['content-type'] = + 'application/x-www-form-urlencoded'; + originalRequest.headers['content-length'] = JSON.stringify( + originalRequest.body, + ).length; + + const request = new Request(originalRequest); + request + .is('application/x-www-form-urlencoded') + .should.eql('application/x-www-form-urlencoded'); + }); + + it('should return false if the content-type is invalid', function () { + const originalRequest = generateBaseRequest(); + originalRequest.headers['content-type'] = + 'application/x-www-form-urlencoded'; + originalRequest.headers['content-length'] = JSON.stringify( + originalRequest.body, + ).length; + + const request = new Request(originalRequest); + request.is('application/json').should.eql(false); + }); }); diff --git a/test/unit/response_test.js b/test/unit/response_test.js index 34644188..36dab673 100644 --- a/test/unit/response_test.js +++ b/test/unit/response_test.js @@ -1,134 +1,134 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const Response = require("../../lib/response"); -const should = require("chai").should(); +const Response = require('../../lib/response'); +const should = require('chai').should(); /** * Test `Request`. */ function generateBaseResponse() { - return { - headers: { - bar: "foo", - }, - body: { - foobar: "barfoo", - }, - }; + return { + headers: { + bar: 'foo', + }, + body: { + foobar: 'barfoo', + }, + }; } -describe("Request", function () { - it("should instantiate with a basic request", function () { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql(originalResponse.body); - response.status.should.eql(200); - }); - - it("should allow a response to be passed without a body", function () { - const originalResponse = generateBaseResponse(); - delete originalResponse.body; - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql({}); - response.status.should.eql(200); - }); - - it("should allow a response to be passed without headers", function () { - const originalResponse = generateBaseResponse(); - delete originalResponse.headers; - - const response = new Response(originalResponse); - response.headers.should.eql({}); - response.body.should.eql(originalResponse.body); - response.status.should.eql(200); - }); - - it("should convert all header keys to lowercase", function () { - const originalResponse = generateBaseResponse(); - originalResponse.headers = { - Foo: "bar", - BAR: "foo", - }; - - const response = new Response(originalResponse); - response.headers.foo.should.eql("bar"); - response.headers.bar.should.eql("foo"); - should.not.exist(response.headers.Foo); - should.not.exist(response.headers.BAR); - }); - - it("should include additional properties passed in the response", function () { - const originalResponse = generateBaseResponse(); - originalResponse.custom = { - newFoo: "newBar", - }; - - originalResponse.custom2 = { - newBar: "newFoo", - }; - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.body.should.eql(originalResponse.body); - response.custom.should.eql(originalResponse.custom); - response.custom2.should.eql(originalResponse.custom2); - }); - - it("should not allow overwriting methods on the Response prototype via custom properties", () => { - const response = new Response({ - headers: { - "content-type": "application/json", - }, - get() { - // malicious attempt to override the 'get' method - return "text/html"; - }, - }); - - response.get("content-type").should.equal("application/json"); - }); - - it("should allow getting of headers using `response.get`", function () { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.get("bar").should.eql(originalResponse.headers.bar); - }); - - it("should allow getting of headers using `response.get`", function () { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.get("bar").should.eql(originalResponse.headers.bar); - }); - - it("should allow setting of headers using `response.set`", function () { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.set("newheader", "newvalue"); - response.headers.bar.should.eql("foo"); - response.headers.newheader.should.eql("newvalue"); - }); - - it("should process redirect", function () { - const originalResponse = generateBaseResponse(); - - const response = new Response(originalResponse); - response.headers.should.eql(originalResponse.headers); - response.status.should.eql(200); - response.redirect("http://foo.bar"); - response.headers.location.should.eql("http://foo.bar"); - response.status.should.eql(302); - }); +describe('Request', function () { + it('should instantiate with a basic request', function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql(originalResponse.body); + response.status.should.eql(200); + }); + + it('should allow a response to be passed without a body', function () { + const originalResponse = generateBaseResponse(); + delete originalResponse.body; + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql({}); + response.status.should.eql(200); + }); + + it('should allow a response to be passed without headers', function () { + const originalResponse = generateBaseResponse(); + delete originalResponse.headers; + + const response = new Response(originalResponse); + response.headers.should.eql({}); + response.body.should.eql(originalResponse.body); + response.status.should.eql(200); + }); + + it('should convert all header keys to lowercase', function () { + const originalResponse = generateBaseResponse(); + originalResponse.headers = { + Foo: 'bar', + BAR: 'foo', + }; + + const response = new Response(originalResponse); + response.headers.foo.should.eql('bar'); + response.headers.bar.should.eql('foo'); + should.not.exist(response.headers.Foo); + should.not.exist(response.headers.BAR); + }); + + it('should include additional properties passed in the response', function () { + const originalResponse = generateBaseResponse(); + originalResponse.custom = { + newFoo: 'newBar', + }; + + originalResponse.custom2 = { + newBar: 'newFoo', + }; + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.body.should.eql(originalResponse.body); + response.custom.should.eql(originalResponse.custom); + response.custom2.should.eql(originalResponse.custom2); + }); + + it('should not allow overwriting methods on the Response prototype via custom properties', () => { + const response = new Response({ + headers: { + 'content-type': 'application/json', + }, + get() { + // malicious attempt to override the 'get' method + return 'text/html'; + }, + }); + + response.get('content-type').should.equal('application/json'); + }); + + it('should allow getting of headers using `response.get`', function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.get('bar').should.eql(originalResponse.headers.bar); + }); + + it('should allow getting of headers using `response.get`', function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.get('bar').should.eql(originalResponse.headers.bar); + }); + + it('should allow setting of headers using `response.set`', function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.set('newheader', 'newvalue'); + response.headers.bar.should.eql('foo'); + response.headers.newheader.should.eql('newvalue'); + }); + + it('should process redirect', function () { + const originalResponse = generateBaseResponse(); + + const response = new Response(originalResponse); + response.headers.should.eql(originalResponse.headers); + response.status.should.eql(200); + response.redirect('http://foo.bar'); + response.headers.location.should.eql('http://foo.bar'); + response.status.should.eql(302); + }); }); diff --git a/test/unit/server_test.js b/test/unit/server_test.js index aaa08c77..379bb477 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -1,78 +1,78 @@ -"use strict"; +'use strict'; /** * Module dependencies. */ -const AuthenticateHandler = require("../../lib/handlers/authenticate-handler"); -const AuthorizeHandler = require("../../lib/handlers/authorize-handler"); -const Server = require("../../lib/server"); -const TokenHandler = require("../../lib/handlers/token-handler"); -const Model = require("../../lib/model"); -const sinon = require("sinon"); +const AuthenticateHandler = require('../../lib/handlers/authenticate-handler'); +const AuthorizeHandler = require('../../lib/handlers/authorize-handler'); +const Server = require('../../lib/server'); +const TokenHandler = require('../../lib/handlers/token-handler'); +const Model = require('../../lib/model'); +const sinon = require('sinon'); /** * Test `Server`. */ -describe("Server", function () { - describe("authenticate()", function () { - it("should call `handle`", function () { - const model = Model.from({ - getAccessToken: function () {}, - }); - const server = new Server({ model: model }); +describe('Server', function () { + describe('authenticate()', function () { + it('should call `handle`', function () { + const model = Model.from({ + getAccessToken: function () {}, + }); + const server = new Server({ model: model }); - sinon - .stub(AuthenticateHandler.prototype, "handle") - .returns(Promise.resolve()); + sinon + .stub(AuthenticateHandler.prototype, 'handle') + .returns(Promise.resolve()); - server.authenticate("foo"); + server.authenticate('foo'); - AuthenticateHandler.prototype.handle.callCount.should.equal(1); - AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal( - "foo", - ); - AuthenticateHandler.prototype.handle.restore(); - }); - }); + AuthenticateHandler.prototype.handle.callCount.should.equal(1); + AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal( + 'foo', + ); + AuthenticateHandler.prototype.handle.restore(); + }); + }); - describe("authorize()", function () { - it("should call `handle`", function () { - const model = Model.from({ - getAccessToken: function () {}, - getClient: function () {}, - saveAuthorizationCode: function () {}, - }); - const server = new Server({ model: model }); + describe('authorize()', function () { + it('should call `handle`', function () { + const model = Model.from({ + getAccessToken: function () {}, + getClient: function () {}, + saveAuthorizationCode: function () {}, + }); + const server = new Server({ model: model }); - sinon - .stub(AuthorizeHandler.prototype, "handle") - .returns(Promise.resolve()); + sinon + .stub(AuthorizeHandler.prototype, 'handle') + .returns(Promise.resolve()); - server.authorize("foo", "bar"); + server.authorize('foo', 'bar'); - AuthorizeHandler.prototype.handle.callCount.should.equal(1); - AuthorizeHandler.prototype.handle.firstCall.args[0].should.equal("foo"); - AuthorizeHandler.prototype.handle.restore(); - }); - }); + AuthorizeHandler.prototype.handle.callCount.should.equal(1); + AuthorizeHandler.prototype.handle.firstCall.args[0].should.equal('foo'); + AuthorizeHandler.prototype.handle.restore(); + }); + }); - describe("token()", function () { - it("should call `handle`", function () { - const model = Model.from({ - getClient: function () {}, - saveToken: function () {}, - }); - const server = new Server({ model: model }); + describe('token()', function () { + it('should call `handle`', function () { + const model = Model.from({ + getClient: function () {}, + saveToken: function () {}, + }); + const server = new Server({ model: model }); - sinon.stub(TokenHandler.prototype, "handle").returns(Promise.resolve()); + sinon.stub(TokenHandler.prototype, 'handle').returns(Promise.resolve()); - server.token("foo", "bar"); + server.token('foo', 'bar'); - TokenHandler.prototype.handle.callCount.should.equal(1); - TokenHandler.prototype.handle.firstCall.args[0].should.equal("foo"); - TokenHandler.prototype.handle.restore(); - }); - }); + TokenHandler.prototype.handle.callCount.should.equal(1); + TokenHandler.prototype.handle.firstCall.args[0].should.equal('foo'); + TokenHandler.prototype.handle.restore(); + }); + }); }); diff --git a/test/unit/utils/crypto-util_test.js b/test/unit/utils/crypto-util_test.js index 04198711..00e8f618 100644 --- a/test/unit/utils/crypto-util_test.js +++ b/test/unit/utils/crypto-util_test.js @@ -1,22 +1,22 @@ -const cryptoUtil = require("../../../lib/utils/crypto-util"); -require("chai").should(); +const cryptoUtil = require('../../../lib/utils/crypto-util'); +require('chai').should(); describe(cryptoUtil.createHash.name, function () { - it("creates a hash by given algorithm", function () { - const data = "client-credentials-grant"; - const hash = cryptoUtil.createHash({ data, output: "hex" }); - hash.should.equal( - "072726830f0aadd2d91f86f53e3a7ef40018c2626438152dd576e272bf2b8e60", - ); - }); - it("should throw if data is missing", function () { - try { - cryptoUtil.createHash({}); - } catch (e) { - e.should.be.instanceOf(TypeError); - e.message.should.include( - 'he "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.', - ); - } - }); + it('creates a hash by given algorithm', function () { + const data = 'client-credentials-grant'; + const hash = cryptoUtil.createHash({ data, output: 'hex' }); + hash.should.equal( + '072726830f0aadd2d91f86f53e3a7ef40018c2626438152dd576e272bf2b8e60', + ); + }); + it('should throw if data is missing', function () { + try { + cryptoUtil.createHash({}); + } catch (e) { + e.should.be.instanceOf(TypeError); + e.message.should.include( + 'he "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.', + ); + } + }); }); diff --git a/test/unit/utils/date-util__test.js b/test/unit/utils/date-util__test.js index 36e82081..f05a204f 100644 --- a/test/unit/utils/date-util__test.js +++ b/test/unit/utils/date-util__test.js @@ -1,26 +1,26 @@ -const dateUtil = require("../../../lib/utils/date-util"); +const dateUtil = require('../../../lib/utils/date-util'); -const sinon = require("sinon"); -require("chai").should(); +const sinon = require('sinon'); +require('chai').should(); -describe("DateUtil", function () { - describe("getLifetimeFromExpiresAt", () => { - const now = new Date("2023-01-01T00:00:00.000Z"); +describe('DateUtil', function () { + describe('getLifetimeFromExpiresAt', () => { + const now = new Date('2023-01-01T00:00:00.000Z'); - beforeEach(() => { - sinon.useFakeTimers(now); - }); + beforeEach(() => { + sinon.useFakeTimers(now); + }); - it("should convert a valid expiration date into seconds from now", () => { - const expiresAt = new Date("2023-01-01T00:00:10.000Z"); - const lifetime = dateUtil.getLifetimeFromExpiresAt(expiresAt); + it('should convert a valid expiration date into seconds from now', () => { + const expiresAt = new Date('2023-01-01T00:00:10.000Z'); + const lifetime = dateUtil.getLifetimeFromExpiresAt(expiresAt); - lifetime.should.be.a("number"); - lifetime.should.be.approximately(10, 2); - }); + lifetime.should.be.a('number'); + lifetime.should.be.approximately(10, 2); + }); - afterEach(() => { - sinon.restore(); - }); - }); + afterEach(() => { + sinon.restore(); + }); + }); }); diff --git a/test/unit/utils/scope-util_test.js b/test/unit/utils/scope-util_test.js index 128672f0..2cc8be3a 100644 --- a/test/unit/utils/scope-util_test.js +++ b/test/unit/utils/scope-util_test.js @@ -1,55 +1,55 @@ -const { parseScope } = require("../../../lib/utils/scope-util"); -const should = require("chai").should(); +const { parseScope } = require('../../../lib/utils/scope-util'); +const should = require('chai').should(); describe(parseScope.name, () => { - it("should return undefined on nullish values", () => { - const values = [undefined, null]; - values.forEach((str) => { - const compare = parseScope(str) === undefined; - compare.should.equal(true); - }); - }); - it("should throw on non-string values", () => { - const invalid = [ - 1, - -1, - true, - false, - {}, - ["foo"], - [], - () => {}, - Symbol("foo"), - ]; - invalid.forEach((str) => { - try { - parseScope(str); - should.fail(); - } catch (e) { - e.message.should.eql("Invalid parameter: `scope`"); - } - }); - }); - it("should throw on empty strings", () => { - const invalid = ["", " ", " ", "\n", "\t", "\r"]; - invalid.forEach((str) => { - try { - parseScope(str); - should.fail(); - } catch (e) { - e.message.should.eql("Invalid parameter: `scope`"); - } - }); - }); - it("should split space-delimited strings into arrays", () => { - const values = [ - ["foo", ["foo"]], - ["foo bar", ["foo", "bar"]], - ["foo bar", ["foo", "bar"]], - ]; - values.forEach(([str, compare]) => { - const parsed = parseScope(str); - parsed.should.deep.equal(compare); - }); - }); + it('should return undefined on nullish values', () => { + const values = [undefined, null]; + values.forEach((str) => { + const compare = parseScope(str) === undefined; + compare.should.equal(true); + }); + }); + it('should throw on non-string values', () => { + const invalid = [ + 1, + -1, + true, + false, + {}, + ['foo'], + [], + () => {}, + Symbol('foo'), + ]; + invalid.forEach((str) => { + try { + parseScope(str); + should.fail(); + } catch (e) { + e.message.should.eql('Invalid parameter: `scope`'); + } + }); + }); + it('should throw on empty strings', () => { + const invalid = ['', ' ', ' ', '\n', '\t', '\r']; + invalid.forEach((str) => { + try { + parseScope(str); + should.fail(); + } catch (e) { + e.message.should.eql('Invalid parameter: `scope`'); + } + }); + }); + it('should split space-delimited strings into arrays', () => { + const values = [ + ['foo', ['foo']], + ['foo bar', ['foo', 'bar']], + ['foo bar', ['foo', 'bar']], + ]; + values.forEach(([str, compare]) => { + const parsed = parseScope(str); + parsed.should.deep.equal(compare); + }); + }); }); From d46d36d13bfc2d5aca1784672a937c21b7f54beb Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Wed, 17 Jun 2026 08:41:19 +0200 Subject: [PATCH 08/11] fix(core): make biome config on par with current format --- biome.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/biome.json b/biome.json index 9319e4b8..3e2821c2 100644 --- a/biome.json +++ b/biome.json @@ -18,7 +18,24 @@ "rules": { "preset": "recommended", "suspicious": { - "noRedundantUseStrict": "off" + "noRedundantUseStrict": "off", + "noPrototypeBuiltins": "off", + "useIsArray": "off" + }, + "complexity": { + "useArrowFunction": "off", + "useDateNow": "off", + "useOptionalChain": "off", + "useLiteralKeys": "off", + "noUselessThisAlias": "off" + }, + "correctness": { + "noUnusedFunctionParameters": "off", + "noUnusedVariables": "off" + }, + "style": { + "useNodejsImportProtocol": "off", + "useTemplate": "off" } } }, From 73fd0f98782bc3ad31e3a2b5c465e05ed8d21b75 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Wed, 17 Jun 2026 08:49:35 +0200 Subject: [PATCH 09/11] fix(core): include index.js for format/lint --- biome.json | 5 +++-- index.js | 1 - .../refresh-token-grant-type_test.js | 20 +++++++++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/biome.json b/biome.json index 3e2821c2..9f124362 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,7 @@ }, "files": { "ignoreUnknown": true, - "includes": ["lib/**/*.js", "test/**/*.js"] + "includes": ["index.js", "lib/**/*.js", "test/**/*.js"] }, "formatter": { "enabled": true, @@ -20,7 +20,8 @@ "suspicious": { "noRedundantUseStrict": "off", "noPrototypeBuiltins": "off", - "useIsArray": "off" + "useIsArray": "off", + "noGlobalAssign": "off" }, "complexity": { "useArrowFunction": "off", diff --git a/index.js b/index.js index f4f940ab..070b74a4 100644 --- a/index.js +++ b/index.js @@ -32,4 +32,3 @@ exports.UnauthorizedClientError = require('./lib/errors/unauthorized-client-erro exports.UnauthorizedRequestError = require('./lib/errors/unauthorized-request-error'); exports.UnsupportedGrantTypeError = require('./lib/errors/unsupported-grant-type-error'); exports.UnsupportedResponseTypeError = require('./lib/errors/unsupported-response-type-error'); - diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index 5d9fd240..6c64d79a 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -248,23 +248,31 @@ describe('RefreshTokenGrantType integration', function () { grantType.handle(request, client).should.be.an.instanceOf(Promise); }); - it('should throw an error if extra `scope` is requested', async function() { + it('should throw an error if extra `scope` is requested', async function () { const client = { id: 123 }; const token = { accessToken: 'foo', client: { id: 123 }, user: { name: 'foo' }, - refreshTokenExpiresAt: new Date(new Date() * 2) + refreshTokenExpiresAt: new Date(new Date() * 2), }; const model = { - getRefreshToken: async function() { + getRefreshToken: async function () { return token; }, revokeToken: () => should.fail(), - saveToken: () => should.fail() + saveToken: () => should.fail(), }; - const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, model }); - const request = new Request({ body: { refresh_token: 'foobar', scope: 'read' }, headers: {}, method: {}, query: {} }); + const grantType = new RefreshTokenGrantType({ + accessTokenLifetime: 123, + model, + }); + const request = new Request({ + body: { refresh_token: 'foobar', scope: 'read' }, + headers: {}, + method: {}, + query: {}, + }); try { await grantType.handle(request, client); From ef7b61f4fd979bab0c36314f5756cf6384dbde22 Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Wed, 17 Jun 2026 09:09:19 +0200 Subject: [PATCH 10/11] fix(core): make formatting line width and array commas on par with original format --- biome.json | 4 +- lib/errors/oauth-error.js | 3 +- lib/grant-types/abstract-grant-type.js | 14 +- .../authorization-code-grant-type.js | 100 +++-------- .../client-credentials-grant-type.js | 30 +--- lib/grant-types/password-grant-type.js | 36 +--- lib/grant-types/refresh-token-grant-type.js | 42 ++--- lib/handlers/authenticate-handler.js | 82 +++------ lib/handlers/authorize-handler.js | 120 ++++--------- lib/handlers/token-handler.js | 60 ++----- lib/models/token-model.js | 18 +- lib/pkce/pkce.js | 14 +- lib/server.js | 6 +- lib/token-types/bearer-token-type.js | 8 +- lib/utils/string-util.js | 6 +- test/compliance/client-authentication_test.js | 7 +- .../client-credential-workflow_test.js | 6 +- test/compliance/password-grant-type_test.js | 14 +- test/compliance/pkce_test.js | 61 +++---- .../refresh-token-grant-type_test.js | 8 +- .../authorization-code-grant-type_test.js | 62 ++----- .../client-credentials-grant-type_test.js | 8 +- .../grant-types/password-grant-type_test.js | 14 +- .../refresh-token-grant-type_test.js | 44 ++--- .../handlers/authenticate-handler_test.js | 68 ++------ .../handlers/authorize-handler_test.js | 157 +++++------------- .../handlers/token-handler_test.js | 79 +++------ .../response-types/code-response-type_test.js | 12 +- .../grant-types/abstract-grant-type_test.js | 4 +- .../authorization-code-grant-type_test.js | 24 +-- .../refresh-token-grant-type_test.js | 4 +- .../handlers/authenticate-handler_test.js | 12 +- test/unit/handlers/authorize-handler_test.js | 23 +-- test/unit/models/token-model_test.js | 6 +- test/unit/pkce/pkce_test.js | 26 +-- test/unit/request_test.js | 24 +-- test/unit/server_test.js | 12 +- test/unit/utils/crypto-util_test.js | 6 +- test/unit/utils/scope-util_test.js | 12 +- 39 files changed, 305 insertions(+), 931 deletions(-) diff --git a/biome.json b/biome.json index 9f124362..907c4160 100644 --- a/biome.json +++ b/biome.json @@ -43,7 +43,9 @@ "javascript": { "formatter": { "quoteStyle": "single", - "indentStyle": "space" + "indentStyle": "space", + "trailingCommas": "es5", + "lineWidth": 120 } }, "assist": { diff --git a/lib/errors/oauth-error.js b/lib/errors/oauth-error.js index e9ee3ae9..c4c33a68 100644 --- a/lib/errors/oauth-error.js +++ b/lib/errors/oauth-error.js @@ -18,8 +18,7 @@ class OAuthError extends Error { constructor(messageOrError, properties) { super(messageOrError, properties); - let message = - messageOrError instanceof Error ? messageOrError.message : messageOrError; + let message = messageOrError instanceof Error ? messageOrError.message : messageOrError; const error = messageOrError instanceof Error ? messageOrError : null; if (properties == null || !Object.entries(properties).length) { diff --git a/lib/grant-types/abstract-grant-type.js b/lib/grant-types/abstract-grant-type.js index cc2f8683..19a4a88b 100644 --- a/lib/grant-types/abstract-grant-type.js +++ b/lib/grant-types/abstract-grant-type.js @@ -28,9 +28,7 @@ class AbstractGrantType { options = options || {}; if (!options.accessTokenLifetime) { - throw new InvalidArgumentError( - 'Missing parameter: `accessTokenLifetime`', - ); + throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); } if (!options.model) { @@ -114,16 +112,10 @@ class AbstractGrantType { */ async validateScope(user, client, scope) { if (this.model.validateScope) { - const validatedScope = await this.model.validateScope( - user, - client, - scope, - ); + const validatedScope = await this.model.validateScope(user, client, scope); if (!validatedScope) { - throw new InvalidScopeError( - 'Invalid scope: Requested scope is invalid', - ); + throw new InvalidScopeError('Invalid scope: Requested scope is invalid'); } return validatedScope; diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index 26150f65..ff336202 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -27,21 +27,15 @@ class AuthorizationCodeGrantType extends AbstractGrantType { } if (!options.model.getAuthorizationCode) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `getAuthorizationCode()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `getAuthorizationCode()`'); } if (!options.model.revokeAuthorizationCode) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `revokeAuthorizationCode()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `revokeAuthorizationCode()`'); } if (!options.model.saveToken) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `saveToken()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); } super(options); @@ -75,12 +69,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType { await this.verifyPKCE(request, code); await this.validateRedirectUri(request, code); - return this.saveToken( - code.user, - client, - code.authorizationCode, - code.scope, - ); + return this.saveToken(code.user, client, code.authorizationCode, code.scope); } /** @@ -102,45 +91,31 @@ class AuthorizationCodeGrantType extends AbstractGrantType { const code = await this.model.getAuthorizationCode(request.body.code); if (!code) { - throw new InvalidGrantError( - 'Invalid grant: authorization code is invalid', - ); + throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } if (!code.client) { - throw new ServerError( - 'Server error: `getAuthorizationCode()` did not return a `client` object', - ); + throw new ServerError('Server error: `getAuthorizationCode()` did not return a `client` object'); } if (!code.user) { - throw new ServerError( - 'Server error: `getAuthorizationCode()` did not return a `user` object', - ); + throw new ServerError('Server error: `getAuthorizationCode()` did not return a `user` object'); } if (code.client.id !== client.id) { - throw new InvalidGrantError( - 'Invalid grant: authorization code is invalid', - ); + throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } if (!(code.expiresAt instanceof Date)) { - throw new ServerError( - 'Server error: `expiresAt` must be a Date instance', - ); + throw new ServerError('Server error: `expiresAt` must be a Date instance'); } if (code.expiresAt < new Date()) { - throw new InvalidGrantError( - 'Invalid grant: authorization code has expired', - ); + throw new InvalidGrantError('Invalid grant: authorization code has expired'); } if (code.redirectUri && !isFormat.uri(code.redirectUri)) { - throw new InvalidGrantError( - 'Invalid grant: `redirect_uri` is not a valid URI', - ); + throw new InvalidGrantError('Invalid grant: `redirect_uri` is not a valid URI'); } return code; @@ -163,9 +138,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType { const method = this.getCodeChallengeMethod(code.codeChallengeMethod); if (!this.enablePlainPKCE && method === 'plain') { - throw new InvalidRequestError( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); + throw new InvalidRequestError('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); } if (!request.body.code_verifier) { @@ -182,9 +155,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType { }); if (!hash) { - throw new ServerError( - 'Server error: no valid hash algorithm available to verify `code_verifier`', - ); + throw new ServerError('Server error: no valid hash algorithm available to verify `code_verifier`'); } // xxx: Use timingSafeEqual to prevent against timing attacks when comparing @@ -201,21 +172,12 @@ class AuthorizationCodeGrantType extends AbstractGrantType { } hashesAreEqual(trusted, untrusted) { - const trustedBuf = Buffer.isBuffer(trusted) - ? trusted - : Buffer.from(trusted); - const untrustedBuf = Buffer.isBuffer(untrusted) - ? untrusted - : Buffer.from(untrusted); + const trustedBuf = Buffer.isBuffer(trusted) ? trusted : Buffer.from(trusted); + const untrustedBuf = Buffer.isBuffer(untrusted) ? untrusted : Buffer.from(untrusted); const equalLength = trustedBuf.byteLength === untrustedBuf.byteLength; // if the buffers are the same length, compare them, // otherwise only compare with the trusted buffer but return false anyway - return ( - crypto.timingSafeEqual( - trustedBuf, - equalLength ? untrustedBuf : trustedBuf, - ) && equalLength - ); + return crypto.timingSafeEqual(trustedBuf, equalLength ? untrustedBuf : trustedBuf) && equalLength; } getCodeChallengeMethod(method) { @@ -247,15 +209,11 @@ class AuthorizationCodeGrantType extends AbstractGrantType { const redirectUri = request.body.redirect_uri || request.query.redirect_uri; if (!isFormat.uri(redirectUri)) { - throw new InvalidRequestError( - 'Invalid request: `redirect_uri` is not a valid URI', - ); + throw new InvalidRequestError('Invalid request: `redirect_uri` is not a valid URI'); } if (redirectUri !== code.redirectUri) { - throw new InvalidRequestError( - 'Invalid request: `redirect_uri` is invalid', - ); + throw new InvalidRequestError('Invalid request: `redirect_uri` is invalid'); } } @@ -273,9 +231,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType { const status = await this.model.revokeAuthorizationCode(code); if (!status) { - throw new InvalidGrantError( - 'Invalid grant: authorization code is invalid', - ); + throw new InvalidGrantError('Invalid grant: authorization code is invalid'); } return code; @@ -292,21 +248,9 @@ class AuthorizationCodeGrantType extends AbstractGrantType { */ async saveToken(user, client, authorizationCode, requestedScope) { - const validatedScope = await this.validateScope( - user, - client, - requestedScope, - ); - const accessToken = await this.generateAccessToken( - client, - user, - validatedScope, - ); - const refreshToken = await this.generateRefreshToken( - client, - user, - validatedScope, - ); + const validatedScope = await this.validateScope(user, client, requestedScope); + const accessToken = await this.generateAccessToken(client, user, validatedScope); + const refreshToken = await this.generateRefreshToken(client, user, validatedScope); const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index 4e4d8d64..c2874f0b 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -19,15 +19,11 @@ class ClientCredentialsGrantType extends AbstractGrantType { } if (!options.model.getUserFromClient) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `getUserFromClient()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `getUserFromClient()`'); } if (!options.model.saveToken) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `saveToken()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); } super(options); @@ -62,9 +58,7 @@ class ClientCredentialsGrantType extends AbstractGrantType { const user = await this.model.getUserFromClient(client); if (!user) { - throw new InvalidGrantError( - 'Invalid grant: user credentials are invalid', - ); + throw new InvalidGrantError('Invalid grant: user credentials are invalid'); } return user; @@ -75,21 +69,9 @@ class ClientCredentialsGrantType extends AbstractGrantType { */ async saveToken(user, client, requestedScope) { - const validatedScope = await this.validateScope( - user, - client, - requestedScope, - ); - const accessToken = await this.generateAccessToken( - client, - user, - validatedScope, - ); - const accessTokenExpiresAt = await this.getAccessTokenExpiresAt( - client, - user, - validatedScope, - ); + const validatedScope = await this.validateScope(user, client, requestedScope); + const accessToken = await this.generateAccessToken(client, user, validatedScope); + const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(client, user, validatedScope); const token = { accessToken, accessTokenExpiresAt, diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index b9da8e79..2c0128a7 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -22,15 +22,11 @@ class PasswordGrantType extends AbstractGrantType { } if (!options.model.getUser) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `getUser()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `getUser()`'); } if (!options.model.saveToken) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `saveToken()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); } super(options); @@ -78,16 +74,10 @@ class PasswordGrantType extends AbstractGrantType { throw new InvalidRequestError('Invalid parameter: `password`'); } - const user = await this.model.getUser( - request.body.username, - request.body.password, - client, - ); + const user = await this.model.getUser(request.body.username, request.body.password, client); if (!user) { - throw new InvalidGrantError( - 'Invalid grant: user credentials are invalid', - ); + throw new InvalidGrantError('Invalid grant: user credentials are invalid'); } return user; @@ -98,21 +88,9 @@ class PasswordGrantType extends AbstractGrantType { */ async saveToken(user, client, requestedScope) { - const validatedScope = await this.validateScope( - user, - client, - requestedScope, - ); - const accessToken = await this.generateAccessToken( - client, - user, - validatedScope, - ); - const refreshToken = await this.generateRefreshToken( - client, - user, - validatedScope, - ); + const validatedScope = await this.validateScope(user, client, requestedScope); + const accessToken = await this.generateAccessToken(client, user, validatedScope); + const refreshToken = await this.generateRefreshToken(client, user, validatedScope); const accessTokenExpiresAt = await this.getAccessTokenExpiresAt(); const refreshTokenExpiresAt = await this.getRefreshTokenExpiresAt(); diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index c6e1b5ad..596a7070 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -23,21 +23,15 @@ class RefreshTokenGrantType extends AbstractGrantType { } if (!options.model.getRefreshToken) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `getRefreshToken()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `getRefreshToken()`'); } if (!options.model.revokeToken) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `revokeToken()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `revokeToken()`'); } if (!options.model.saveToken) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `saveToken()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `saveToken()`'); } super(options); @@ -89,36 +83,22 @@ class RefreshTokenGrantType extends AbstractGrantType { } if (!token.client) { - throw new ServerError( - 'Server error: `getRefreshToken()` did not return a `client` object', - ); + throw new ServerError('Server error: `getRefreshToken()` did not return a `client` object'); } if (!token.user) { - throw new ServerError( - 'Server error: `getRefreshToken()` did not return a `user` object', - ); + throw new ServerError('Server error: `getRefreshToken()` did not return a `user` object'); } if (token.client.id !== client.id) { - throw new InvalidGrantError( - 'Invalid grant: refresh token was issued to another client', - ); + throw new InvalidGrantError('Invalid grant: refresh token was issued to another client'); } - if ( - token.refreshTokenExpiresAt && - !(token.refreshTokenExpiresAt instanceof Date) - ) { - throw new ServerError( - 'Server error: `refreshTokenExpiresAt` must be a Date instance', - ); + if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) { + throw new ServerError('Server error: `refreshTokenExpiresAt` must be a Date instance'); } - if ( - token.refreshTokenExpiresAt && - token.refreshTokenExpiresAt < new Date() - ) { + if (token.refreshTokenExpiresAt && token.refreshTokenExpiresAt < new Date()) { throw new InvalidGrantError('Invalid grant: refresh token has expired'); } @@ -139,9 +119,7 @@ class RefreshTokenGrantType extends AbstractGrantType { const status = await this.model.revokeToken(token); if (!status) { - throw new InvalidGrantError( - 'Invalid grant: refresh token is invalid or could not be revoked', - ); + throw new InvalidGrantError('Invalid grant: refresh token is invalid or could not be revoked'); } return token; diff --git a/lib/handlers/authenticate-handler.js b/lib/handlers/authenticate-handler.js index 3dd9f291..4072f385 100644 --- a/lib/handlers/authenticate-handler.js +++ b/lib/handlers/authenticate-handler.js @@ -38,37 +38,26 @@ class AuthenticateHandler { } if (!options.model.getAccessToken) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `getAccessToken()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `getAccessToken()`'); } if (options.scope && undefined === options.addAcceptedScopesHeader) { - throw new InvalidArgumentError( - 'Missing parameter: `addAcceptedScopesHeader`', - ); + throw new InvalidArgumentError('Missing parameter: `addAcceptedScopesHeader`'); } if (options.scope && undefined === options.addAuthorizedScopesHeader) { - throw new InvalidArgumentError( - 'Missing parameter: `addAuthorizedScopesHeader`', - ); + throw new InvalidArgumentError('Missing parameter: `addAuthorizedScopesHeader`'); } if (options.scope && !options.model.verifyScope) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `verifyScope()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `verifyScope()`'); } this.addAcceptedScopesHeader = options.addAcceptedScopesHeader; this.addAuthorizedScopesHeader = options.addAuthorizedScopesHeader; - this.allowBearerTokensInQueryString = - options.allowBearerTokensInQueryString; + this.allowBearerTokensInQueryString = options.allowBearerTokensInQueryString; this.model = options.model; - this.scope = Array.isArray(options.scope) - ? options.scope - : parseScope(options.scope); + this.scope = Array.isArray(options.scope) ? options.scope : parseScope(options.scope); } /** @@ -79,15 +68,11 @@ class AuthenticateHandler { */ async handle(request, response) { if (!(request instanceof Request)) { - throw new InvalidArgumentError( - 'Invalid argument: `request` must be an instance of Request', - ); + throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); } if (!(response instanceof Response)) { - throw new InvalidArgumentError( - 'Invalid argument: `response` must be an instance of Response', - ); + throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); } try { @@ -112,20 +97,11 @@ class AuthenticateHandler { if (e instanceof UnauthorizedRequestError) { response.set('WWW-Authenticate', 'Bearer realm="Service"'); } else if (e instanceof InvalidRequestError) { - response.set( - 'WWW-Authenticate', - 'Bearer realm="Service",error="invalid_request"', - ); + response.set('WWW-Authenticate', 'Bearer realm="Service",error="invalid_request"'); } else if (e instanceof InvalidTokenError) { - response.set( - 'WWW-Authenticate', - 'Bearer realm="Service",error="invalid_token"', - ); + response.set('WWW-Authenticate', 'Bearer realm="Service",error="invalid_token"'); } else if (e instanceof InsufficientScopeError) { - response.set( - 'WWW-Authenticate', - 'Bearer realm="Service",error="insufficient_scope"', - ); + response.set('WWW-Authenticate', 'Bearer realm="Service",error="insufficient_scope"'); } if (!(e instanceof OAuthError)) { @@ -150,9 +126,7 @@ class AuthenticateHandler { const bodyToken = request.body.access_token; if (!!headerToken + !!queryToken + !!bodyToken > 1) { - throw new InvalidRequestError( - 'Invalid request: only one authentication method is allowed', - ); + throw new InvalidRequestError('Invalid request: only one authentication method is allowed'); } if (headerToken) { @@ -167,9 +141,7 @@ class AuthenticateHandler { return this.getTokenFromRequestBody(request); } - throw new UnauthorizedRequestError( - 'Unauthorized request: no authentication given', - ); + throw new UnauthorizedRequestError('Unauthorized request: no authentication given'); } /** @@ -184,9 +156,7 @@ class AuthenticateHandler { const matches = token.match(/^Bearer ([0-9a-zA-Z-._~+/]+=*)$/); if (!matches) { - throw new InvalidRequestError( - 'Invalid request: malformed authorization header', - ); + throw new InvalidRequestError('Invalid request: malformed authorization header'); } return matches[1]; @@ -209,9 +179,7 @@ class AuthenticateHandler { getTokenFromRequestQuery(request) { if (!this.allowBearerTokensInQueryString) { - throw new InvalidRequestError( - 'Invalid request: do not send bearer tokens in query URLs', - ); + throw new InvalidRequestError('Invalid request: do not send bearer tokens in query URLs'); } return request.query.access_token; @@ -228,15 +196,11 @@ class AuthenticateHandler { getTokenFromRequestBody(request) { if (request.method === 'GET') { - throw new InvalidRequestError( - 'Invalid request: token may not be passed in the body when using the GET verb', - ); + throw new InvalidRequestError('Invalid request: token may not be passed in the body when using the GET verb'); } if (!request.is('application/x-www-form-urlencoded')) { - throw new InvalidRequestError( - 'Invalid request: content must be application/x-www-form-urlencoded', - ); + throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded'); } return request.body.access_token; @@ -255,9 +219,7 @@ class AuthenticateHandler { } if (!accessToken.user) { - throw new ServerError( - 'Server error: `getAccessToken()` did not return a `user` object', - ); + throw new ServerError('Server error: `getAccessToken()` did not return a `user` object'); } return accessToken; @@ -269,9 +231,7 @@ class AuthenticateHandler { validateAccessToken(accessToken) { if (!(accessToken.accessTokenExpiresAt instanceof Date)) { - throw new ServerError( - 'Server error: `accessTokenExpiresAt` must be a Date instance', - ); + throw new ServerError('Server error: `accessTokenExpiresAt` must be a Date instance'); } if (accessToken.accessTokenExpiresAt < new Date()) { @@ -289,9 +249,7 @@ class AuthenticateHandler { const scope = await this.model.verifyScope(accessToken, this.scope); if (!scope) { - throw new InsufficientScopeError( - 'Insufficient scope: authorized scope is insufficient', - ); + throw new InsufficientScopeError('Insufficient scope: authorized scope is insufficient'); } } diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index 463ba799..bdba9831 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -40,15 +40,11 @@ class AuthorizeHandler { options = options || {}; if (options.authenticateHandler && !options.authenticateHandler.handle) { - throw new InvalidArgumentError( - 'Invalid argument: authenticateHandler does not implement `handle()`', - ); + throw new InvalidArgumentError('Invalid argument: authenticateHandler does not implement `handle()`'); } if (!options.authorizationCodeLifetime) { - throw new InvalidArgumentError( - 'Missing parameter: `authorizationCodeLifetime`', - ); + throw new InvalidArgumentError('Missing parameter: `authorizationCodeLifetime`'); } if (!options.model) { @@ -56,20 +52,15 @@ class AuthorizeHandler { } if (!options.model.getClient) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `getClient()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); } if (!options.model.saveAuthorizationCode) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `saveAuthorizationCode()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `saveAuthorizationCode()`'); } this.allowEmptyState = options.allowEmptyState; - this.authenticateHandler = - options.authenticateHandler || new AuthenticateHandler(options); + this.authenticateHandler = options.authenticateHandler || new AuthenticateHandler(options); this.authorizationCodeLifetime = options.authorizationCodeLifetime; this.enablePlainPKCE = options.enablePlainPKCE === true; this.model = options.model; @@ -81,15 +72,11 @@ class AuthorizeHandler { async handle(request, response) { if (!(request instanceof Request)) { - throw new InvalidArgumentError( - 'Invalid argument: `request` must be an instance of Request', - ); + throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); } if (!(response instanceof Response)) { - throw new InvalidArgumentError( - 'Invalid argument: `response` must be an instance of Response', - ); + throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); } const expiresAt = await this.getAuthorizationCodeLifetime(); @@ -103,22 +90,13 @@ class AuthorizeHandler { uri = this.getRedirectUri(request, client); state = this.getState(request); - if ( - request.query.allowed === 'false' || - request.body.allowed === 'false' - ) { - throw new AccessDeniedError( - 'Access denied: user denied access to application', - ); + if (request.query.allowed === 'false' || request.body.allowed === 'false') { + throw new AccessDeniedError('Access denied: user denied access to application'); } const requestedScope = await this.getScope(request); const validScope = await this.validateScope(user, client, requestedScope); - const authorizationCode = await this.generateAuthorizationCode( - client, - user, - validScope, - ); + const authorizationCode = await this.generateAuthorizationCode(client, user, validScope); const ResponseType = this.getResponseType(request); const codeChallenge = this.getCodeChallenge(request); @@ -131,14 +109,11 @@ class AuthorizeHandler { uri, user, codeChallenge, - codeChallengeMethod, + codeChallengeMethod ); const responseTypeInstance = new ResponseType(code.authorizationCode); - const redirectUri = this.buildSuccessRedirectUri( - uri, - responseTypeInstance, - ); + const redirectUri = this.buildSuccessRedirectUri(uri, responseTypeInstance); this.updateResponse(response, redirectUri, state); @@ -197,45 +172,32 @@ class AuthorizeHandler { const redirectUri = request.body.redirect_uri || request.query.redirect_uri; if (redirectUri && !isFormat.uri(redirectUri)) { - throw new InvalidRequestError( - 'Invalid request: `redirect_uri` is not a valid URI', - ); + throw new InvalidRequestError('Invalid request: `redirect_uri` is not a valid URI'); } const client = await this.model.getClient(clientId, null); if (!client) { - throw new InvalidClientError( - 'Invalid client: client credentials are invalid', - ); + throw new InvalidClientError('Invalid client: client credentials are invalid'); } if (!client.grants) { throw new InvalidClientError('Invalid client: missing client `grants`'); } - if ( - !Array.isArray(client.grants) || - !client.grants.includes('authorization_code') - ) { - throw new UnauthorizedClientError( - 'Unauthorized client: `grant_type` is invalid', - ); + if (!Array.isArray(client.grants) || !client.grants.includes('authorization_code')) { + throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); } if (!client.redirectUris || 0 === client.redirectUris.length) { - throw new InvalidClientError( - 'Invalid client: missing client `redirectUri`', - ); + throw new InvalidClientError('Invalid client: missing client `redirectUri`'); } if (redirectUri) { const valid = await self.validateRedirectUri(redirectUri, client); if (!valid) { - throw new InvalidClientError( - 'Invalid client: `redirect_uri` does not match client value', - ); + throw new InvalidClientError('Invalid client: `redirect_uri` does not match client value'); } } @@ -247,16 +209,10 @@ class AuthorizeHandler { */ async validateScope(user, client, scope) { if (this.model.validateScope) { - const validatedScope = await this.model.validateScope( - user, - client, - scope, - ); + const validatedScope = await this.model.validateScope(user, client, scope); if (!validatedScope) { - throw new InvalidScopeError( - 'Invalid scope: Requested scope is invalid', - ); + throw new InvalidScopeError('Invalid scope: Requested scope is invalid'); } return validatedScope; @@ -282,9 +238,7 @@ class AuthorizeHandler { getState(request) { const state = request.body.state || request.query.state; const stateExists = state && state.length > 0; - const stateIsValid = stateExists - ? isFormat.vschar(state) - : this.allowEmptyState; + const stateIsValid = stateExists ? isFormat.vschar(state) : this.allowEmptyState; if (!stateIsValid) { const message = !stateExists ? 'Missing' : 'Invalid'; @@ -307,9 +261,7 @@ class AuthorizeHandler { const user = await this.authenticateHandler.handle(request, response); if (!user) { - throw new ServerError( - 'Server error: `handle()` did not return a `user` object', - ); + throw new ServerError('Server error: `handle()` did not return a `user` object'); } return user; @@ -320,11 +272,7 @@ class AuthorizeHandler { */ getRedirectUri(request, client) { - return ( - request.body.redirect_uri || - request.query.redirect_uri || - client.redirectUris[0] - ); + return request.body.redirect_uri || request.query.redirect_uri || client.redirectUris[0]; } /** @@ -339,7 +287,7 @@ class AuthorizeHandler { redirectUri, user, codeChallenge, - codeChallengeMethod, + codeChallengeMethod ) { let code = { authorizationCode: authorizationCode, @@ -354,7 +302,7 @@ class AuthorizeHandler { codeChallenge: codeChallenge, codeChallengeMethod: codeChallengeMethod, }, - code, + code ); } @@ -373,17 +321,14 @@ class AuthorizeHandler { */ getResponseType(request) { - const responseType = - request.body.response_type || request.query.response_type; + const responseType = request.body.response_type || request.query.response_type; if (!responseType) { throw new InvalidRequestError('Missing parameter: `response_type`'); } if (!Object.prototype.hasOwnProperty.call(responseTypes, responseType)) { - throw new UnsupportedResponseTypeError( - 'Unsupported response type: `response_type` is not supported', - ); + throw new UnsupportedResponseTypeError('Unsupported response type: `response_type` is not supported'); } return responseTypes[responseType]; @@ -446,19 +391,14 @@ class AuthorizeHandler { * (see https://www.rfc-editor.org/rfc/rfc7636#section-4.4) */ getCodeChallengeMethod(request) { - const algorithm = - request.body.code_challenge_method || request.query.code_challenge_method; + const algorithm = request.body.code_challenge_method || request.query.code_challenge_method; if (algorithm && !pkce.isValidMethod(algorithm)) { - throw new InvalidRequestError( - `Invalid request: transform algorithm '${algorithm}' not supported`, - ); + throw new InvalidRequestError(`Invalid request: transform algorithm '${algorithm}' not supported`); } if (!this.enablePlainPKCE && algorithm === 'plain') { - throw new InvalidRequestError( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); + throw new InvalidRequestError('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); } // return the verified algorithm, if provided diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index db56a6e6..8eac7aac 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -39,9 +39,7 @@ class TokenHandler { options = options || {}; if (!options.accessTokenLifetime) { - throw new InvalidArgumentError( - 'Missing parameter: `accessTokenLifetime`', - ); + throw new InvalidArgumentError('Missing parameter: `accessTokenLifetime`'); } if (!options.model) { @@ -49,15 +47,11 @@ class TokenHandler { } if (!options.refreshTokenLifetime) { - throw new InvalidArgumentError( - 'Missing parameter: `refreshTokenLifetime`', - ); + throw new InvalidArgumentError('Missing parameter: `refreshTokenLifetime`'); } if (!options.model.getClient) { - throw new InvalidArgumentError( - 'Invalid argument: model does not implement `getClient()`', - ); + throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`'); } this.accessTokenLifetime = options.accessTokenLifetime; @@ -65,10 +59,8 @@ class TokenHandler { this.model = options.model; this.refreshTokenLifetime = options.refreshTokenLifetime; this.allowExtendedTokenAttributes = options.allowExtendedTokenAttributes; - this.requireClientAuthentication = - options.requireClientAuthentication || {}; - this.alwaysIssueNewRefreshToken = - options.alwaysIssueNewRefreshToken !== false; + this.requireClientAuthentication = options.requireClientAuthentication || {}; + this.alwaysIssueNewRefreshToken = options.alwaysIssueNewRefreshToken !== false; this.enablePlainPKCE = options.enablePlainPKCE === true; } @@ -78,15 +70,11 @@ class TokenHandler { async handle(request, response) { if (!(request instanceof Request)) { - throw new InvalidArgumentError( - 'Invalid argument: `request` must be an instance of Request', - ); + throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request'); } if (!(response instanceof Response)) { - throw new InvalidArgumentError( - 'Invalid argument: `response` must be an instance of Response', - ); + throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response'); } if (request.method !== 'POST') { @@ -94,9 +82,7 @@ class TokenHandler { } if (!request.is('application/x-www-form-urlencoded')) { - throw new InvalidRequestError( - 'Invalid request: content must be application/x-www-form-urlencoded', - ); + throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded'); } try { @@ -136,11 +122,7 @@ class TokenHandler { throw new InvalidRequestError('Missing parameter: `client_id`'); } - if ( - this.isClientAuthenticationRequired(grantType) && - !credentials.clientSecret && - !isPkce - ) { + if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret && !isPkce) { throw new InvalidRequestError('Missing parameter: `client_secret`'); } @@ -148,18 +130,12 @@ class TokenHandler { throw new InvalidRequestError('Invalid parameter: `client_id`'); } - if ( - credentials.clientSecret && - !isFormat.vschar(credentials.clientSecret) - ) { + if (credentials.clientSecret && !isFormat.vschar(credentials.clientSecret)) { throw new InvalidRequestError('Invalid parameter: `client_secret`'); } try { - const client = await this.model.getClient( - credentials.clientId, - credentials.clientSecret, - ); + const client = await this.model.getClient(credentials.clientId, credentials.clientSecret); if (!client) { throw new InvalidClientError('Invalid client: client is invalid'); @@ -225,9 +201,7 @@ class TokenHandler { } } - throw new InvalidClientError( - 'Invalid client: cannot retrieve client credentials', - ); + throw new InvalidClientError('Invalid client: cannot retrieve client credentials'); } /** @@ -246,15 +220,11 @@ class TokenHandler { } if (!Object.prototype.hasOwnProperty.call(this.grantTypes, grantType)) { - throw new UnsupportedGrantTypeError( - 'Unsupported grant type: `grant_type` is invalid', - ); + throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid'); } if (!Array.isArray(client.grants) || !client.grants.includes(grantType)) { - throw new UnauthorizedClientError( - 'Unauthorized client: `grant_type` is invalid', - ); + throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid'); } const accessTokenLifetime = this.getAccessTokenLifetime(client); @@ -298,7 +268,7 @@ class TokenHandler { model.accessTokenLifetime, model.refreshToken, model.scope, - model.customAttributes, + model.customAttributes ); } diff --git a/lib/models/token-model.js b/lib/models/token-model.js index d7ac117d..12dd3bb5 100644 --- a/lib/models/token-model.js +++ b/lib/models/token-model.js @@ -33,15 +33,7 @@ class TokenModel { * @param options */ constructor(data = {}, options = {}) { - const { - accessToken, - accessTokenExpiresAt, - refreshToken, - refreshTokenExpiresAt, - scope, - client, - user, - } = data; + const { accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, scope, client, user } = data; if (!accessToken) { throw new InvalidArgumentError('Missing parameter: `accessToken`'); @@ -56,15 +48,11 @@ class TokenModel { } if (accessTokenExpiresAt && !(accessTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError( - 'Invalid parameter: `accessTokenExpiresAt`', - ); + throw new InvalidArgumentError('Invalid parameter: `accessTokenExpiresAt`'); } if (refreshTokenExpiresAt && !(refreshTokenExpiresAt instanceof Date)) { - throw new InvalidArgumentError( - 'Invalid parameter: `refreshTokenExpiresAt`', - ); + throw new InvalidArgumentError('Invalid parameter: `refreshTokenExpiresAt`'); } this.accessToken = accessToken; diff --git a/lib/pkce/pkce.js b/lib/pkce/pkce.js index a822a448..303d0123 100644 --- a/lib/pkce/pkce.js +++ b/lib/pkce/pkce.js @@ -15,8 +15,7 @@ const { createHash } = require('../utils/crypto-util'); * DIGIT = %x30-39 * @type {RegExp} */ -const codeChallengeAndVerifierRegexp = - /^([\u0041-\u005a\u0061-\u007A0-9.\-_~]){43,128}$/; +const codeChallengeAndVerifierRegexp = /^([\u0041-\u005a\u0061-\u007A0-9.\-_~]){43,128}$/; /** * @module pkce @@ -33,11 +32,7 @@ const codeChallengeAndVerifierRegexp = function getHashForCodeChallenge({ method, verifier }) { // to prevent undesired side-effects when passing some weird values // to createHash or base64URLEncode we first check if the values are right - if ( - isValidMethod(method) && - typeof verifier === 'string' && - verifier.length > 0 - ) { + if (isValidMethod(method) && typeof verifier === 'string' && verifier.length > 0) { if (method === 'plain') { return verifier; } @@ -62,10 +57,7 @@ function getHashForCodeChallenge({ method, verifier }) { * @return {Boolean} */ function codeChallengeMatchesABNF(codeChallenge) { - return ( - typeof codeChallenge === 'string' && - codeChallengeAndVerifierRegexp.test(codeChallenge) - ); + return typeof codeChallenge === 'string' && codeChallengeAndVerifierRegexp.test(codeChallenge); } /** diff --git a/lib/server.js b/lib/server.js index 9afd409d..6d52c1d5 100644 --- a/lib/server.js +++ b/lib/server.js @@ -105,7 +105,7 @@ class OAuth2Server { allowBearerTokensInQueryString: false, }, this.options, - options, + options ); return new AuthenticateHandler(options).handle(request, response); @@ -173,7 +173,7 @@ class OAuth2Server { authorizationCodeLifetime: 5 * 60, // 5 minutes. }, this.options, - options, + options ); return new AuthorizeHandler(options).handle(request, response); @@ -240,7 +240,7 @@ class OAuth2Server { requireClientAuthentication: {}, // defaults to true for all grant types }, this.options, - options, + options ); return new TokenHandler(options).handle(request, response); diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js index fe76b0d7..91d338ce 100644 --- a/lib/token-types/bearer-token-type.js +++ b/lib/token-types/bearer-token-type.js @@ -19,13 +19,7 @@ class BearerTokenType { * @param scope * @param customAttributes */ - constructor( - accessToken, - accessTokenLifetime, - refreshToken, - scope, - customAttributes, - ) { + constructor(accessToken, accessTokenLifetime, refreshToken, scope, customAttributes) { if (!accessToken) { throw new InvalidArgumentError('Missing parameter: `accessToken`'); } diff --git a/lib/utils/string-util.js b/lib/utils/string-util.js index cc514e08..3d0f26e9 100644 --- a/lib/utils/string-util.js +++ b/lib/utils/string-util.js @@ -11,11 +11,7 @@ * @return {string} */ function base64URLEncode(str) { - return str - .toString('base64') - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); + return str.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } module.exports = { base64URLEncode }; diff --git a/test/compliance/client-authentication_test.js b/test/compliance/client-authentication_test.js index 2bd299d0..a45363b4 100644 --- a/test/compliance/client-authentication_test.js +++ b/test/compliance/client-authentication_test.js @@ -45,9 +45,7 @@ function createDefaultRequest() { scope, }, headers: { - authorization: - 'Basic ' + - Buffer.from(client.id + ':' + client.secret).toString('base64'), + authorization: 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), 'content-type': 'application/x-www-form-urlencoded', }, method: 'POST', @@ -85,8 +83,7 @@ describe('Client Authentication Compliance', function () { const request = createDefaultRequest(); const response = new Response({}); - request.headers.authorization = - 'Basic ' + Buffer.from('a:c').toString('base64'); + request.headers.authorization = 'Basic ' + Buffer.from('a:c').toString('base64'); await auth .token(request, response, {}) diff --git a/test/compliance/client-credential-workflow_test.js b/test/compliance/client-credential-workflow_test.js index 154df0a1..df5865f8 100644 --- a/test/compliance/client-credential-workflow_test.js +++ b/test/compliance/client-credential-workflow_test.js @@ -77,11 +77,7 @@ describe('ClientCredentials Workflow Compliance (4.4)', function () { scope: enabledScope, }, headers: { - authorization: - 'Basic ' + - Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString( - 'base64', - ), + authorization: 'Basic ' + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), 'content-type': 'application/x-www-form-urlencoded', }, method: 'POST', diff --git a/test/compliance/password-grant-type_test.js b/test/compliance/password-grant-type_test.js index 2596ae1f..3edf9bbf 100644 --- a/test/compliance/password-grant-type_test.js +++ b/test/compliance/password-grant-type_test.js @@ -83,9 +83,7 @@ function createDefaultRequest() { scope, }, headers: { - authorization: - 'Basic ' + - Buffer.from(client.id + ':' + client.secret).toString('base64'), + authorization: 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), 'content-type': 'application/x-www-form-urlencoded', }, method: 'POST', @@ -131,11 +129,7 @@ describe('PasswordGrantType Compliance', function () { }); const authenticationResponse = new Response({}); - const authenticated = await auth.authenticate( - authenticationRequest, - authenticationResponse, - {}, - ); + const authenticated = await auth.authenticate(authenticationRequest, authenticationResponse, {}); authenticated.scope.should.eql(['read', 'write']); authenticated.user.should.be.an('object'); @@ -193,9 +187,7 @@ describe('PasswordGrantType Compliance', function () { const clientId = crypto.randomBytes(4).toString('hex'); const clientSecret = crypto.randomBytes(4).toString('hex'); - request.headers.authorization = - 'Basic ' + - Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); + request.headers.authorization = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); await auth.token(request, response, {}).catch((err) => { err.name.should.equal('invalid_client'); diff --git a/test/compliance/pkce_test.js b/test/compliance/pkce_test.js index 24f6b7cd..d6901749 100644 --- a/test/compliance/pkce_test.js +++ b/test/compliance/pkce_test.js @@ -116,9 +116,7 @@ describe('PKCE Compliance (RFC 7636)', function () { code_verifier: codeVerifier, }, headers: { - authorization: - 'Basic ' + - Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), + authorization: 'Basic ' + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), 'content-type': 'application/x-www-form-urlencoded', }, method: 'POST', @@ -212,7 +210,7 @@ describe('PKCE Compliance (RFC 7636)', function () { throw new Error( 'Server issued a token for a 1-character code_verifier ("z"). ' + 'RFC 7636 §4.1 ABNF requires 43..128 unreserved characters; accepting shorter ' + - 'values breaks the entropy guarantee of Appendix B.', + 'values breaks the entropy guarantee of Appendix B.' ); } }); @@ -238,7 +236,7 @@ describe('PKCE Compliance (RFC 7636)', function () { throw new Error( 'Server issued a token for a 42-character code_verifier. ' + 'RFC 7636 §4.1 ABNF minimum is 43 characters; server-side enforcement ' + - 'is needed to preserve the entropy guarantee of Appendix B.', + 'is needed to preserve the entropy guarantee of Appendix B.' ); } }); @@ -264,7 +262,7 @@ describe('PKCE Compliance (RFC 7636)', function () { throw new Error( 'Server issued a token for a 129-character code_verifier. ' + 'RFC 7636 §4.1 ABNF maximum is 128 characters; server-side enforcement ' + - 'is needed to preserve the entropy guarantee of Appendix B.', + 'is needed to preserve the entropy guarantee of Appendix B.' ); } }); @@ -289,7 +287,7 @@ describe('PKCE Compliance (RFC 7636)', function () { if (tokenIssued) { throw new Error( 'Server issued a token for a code_verifier containing ' + - 'forbidden characters (space). RFC 7636 §4.1 restricts to unreserved characters.', + 'forbidden characters (space). RFC 7636 §4.1 restricts to unreserved characters.' ); } }); @@ -328,10 +326,7 @@ describe('PKCE Compliance (RFC 7636)', function () { // before const codeExists = db.authorizationCodes.has(code.authorizationCode); - codeExists.should.equal( - true, - 'Precondition failed: seeded authorization code should exist in DB', - ); + codeExists.should.equal(true, 'Precondition failed: seeded authorization code should exist in DB'); try { await oAuth2Server.token(badRequest, badResponse); @@ -346,8 +341,7 @@ describe('PKCE Compliance (RFC 7636)', function () { if (codeStillExists) { throw new Error( - 'Authorization code was NOT revoked after a failed ' + - 'code_verifier attempt. An attacker can keep guessing.', + 'Authorization code was NOT revoked after a failed ' + 'code_verifier attempt. An attacker can keep guessing.' ); } }); @@ -383,7 +377,7 @@ describe('PKCE Compliance (RFC 7636)', function () { throw new Error( `Brute-forced code_verifier in ${tries} tries ` + `(guess="${successfulGuess}"). The authorization code was not ` + - 'consumed after failed attempts, allowing online guessing.', + 'consumed after failed attempts, allowing online guessing.' ); } }); @@ -407,10 +401,7 @@ describe('PKCE Compliance (RFC 7636)', function () { } // Attempt 2: correct verifier but should fail because code was revoked - const correctRequest = tokenRequest( - code.authorizationCode, - validVerifier, - ); + const correctRequest = tokenRequest(code.authorizationCode, validVerifier); const correctResponse = new Response(); let tokenIssued = false; @@ -430,7 +421,7 @@ describe('PKCE Compliance (RFC 7636)', function () { throw new Error( 'Authorization code was still valid after a prior ' + 'failed PKCE attempt. The code should have been revoked on the first ' + - 'failed verification to prevent further guessing.', + 'failed verification to prevent further guessing.' ); } }); @@ -475,8 +466,7 @@ describe('PKCE Compliance (RFC 7636)', function () { // We seed an authorization code using "plain" (which is what the // server would store when code_challenge_method is omitted per // RFC 7636 §4.3). - const codeValue = - 'auth-code-plain-default-' + Math.random().toString(36).slice(2); + const codeValue = 'auth-code-plain-default-' + Math.random().toString(36).slice(2); const codeDoc = { authorizationCode: codeValue, expiresAt: new Date(Date.now() + 60000), @@ -501,9 +491,7 @@ describe('PKCE Compliance (RFC 7636)', function () { } catch (e) { // would be expected if plain were rejected e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); + e.message.should.equal('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); } // Note: accepting "plain" is RFC 7636-compliant (§4.3 says the @@ -516,7 +504,7 @@ describe('PKCE Compliance (RFC 7636)', function () { 'Server issued a token using "plain" PKCE method. ' + 'While RFC 7636 §4.3 requires server support for "plain", the OAuth 2.0 ' + 'Security BCP and OAuth 2.1 deprecate it because code_challenge === code_verifier ' + - 'offers zero cryptographic protection.', + 'offers zero cryptographic protection.' ); } }); @@ -551,8 +539,7 @@ describe('PKCE Compliance (RFC 7636)', function () { }); const verifier = 'a'.repeat(43); // valid ABNF-length verifier - const codeValue = - 'auth-code-reject-plain-' + Math.random().toString(36).slice(2); + const codeValue = 'auth-code-reject-plain-' + Math.random().toString(36).slice(2); db.authorizationCodes.set(codeValue, { authorizationCode: codeValue, expiresAt: new Date(Date.now() + 60000), @@ -581,7 +568,7 @@ describe('PKCE Compliance (RFC 7636)', function () { throw new Error( 'Server with enablePlainPKCE=false still issued ' + 'a token using "plain" PKCE method. The option should cause the server to ' + - 'reject any plain code_challenge_method.', + 'reject any plain code_challenge_method.' ); } @@ -603,8 +590,7 @@ describe('PKCE Compliance (RFC 7636)', function () { // omitted (defaults to "plain"): code_challenge = verifier const stolenChallenge = verifier; // attacker reads this from the authorize request - const codeValue = - 'auth-code-stolen-' + Math.random().toString(36).slice(2); + const codeValue = 'auth-code-stolen-' + Math.random().toString(36).slice(2); db.authorizationCodes.set(codeValue, { authorizationCode: codeValue, expiresAt: new Date(Date.now() + 60000), @@ -627,16 +613,14 @@ describe('PKCE Compliance (RFC 7636)', function () { tokenIssued = true; } catch (e) { e.should.be.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); + e.message.should.equal('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); } if (tokenIssued) { throw new Error( 'Attacker redeemed an authorization code by using ' + 'the intercepted code_challenge as code_verifier (plain method). ' + - 'This defeats PKCE entirely for public clients.', + 'This defeats PKCE entirely for public clients.' ); } }); @@ -654,8 +638,7 @@ describe('PKCE Compliance (RFC 7636)', function () { // omitted (defaults to "plain"): code_challenge = verifier const stolenChallenge = verifier; // attacker reads this from the authorize request - const codeValue = - 'auth-code-stolen-' + Math.random().toString(36).slice(2); + const codeValue = 'auth-code-stolen-' + Math.random().toString(36).slice(2); db.authorizationCodes.set(codeValue, { authorizationCode: codeValue, expiresAt: new Date(Date.now() + 60000), @@ -679,16 +662,14 @@ describe('PKCE Compliance (RFC 7636)', function () { } catch (e) { // this is not part of the standard which is why we throw a generic ServerError e.should.be.instanceOf(ServerError); - e.message.should.equal( - 'Server error: no valid hash algorithm available to verify `code_verifier`', - ); + e.message.should.equal('Server error: no valid hash algorithm available to verify `code_verifier`'); } if (tokenIssued) { throw new Error( 'Attacker redeemed an authorization code by using ' + 'the intercepted code_challenge as code_verifier (custom method). ' + - 'This defeats PKCE entirely for public clients.', + 'This defeats PKCE entirely for public clients.' ); } }); diff --git a/test/compliance/refresh-token-grant-type_test.js b/test/compliance/refresh-token-grant-type_test.js index 67e2deb2..0e8c1627 100644 --- a/test/compliance/refresh-token-grant-type_test.js +++ b/test/compliance/refresh-token-grant-type_test.js @@ -88,9 +88,7 @@ function createLoginRequest() { scope, }, headers: { - authorization: - 'Basic ' + - Buffer.from(client.id + ':' + client.secret).toString('base64'), + authorization: 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), 'content-type': 'application/x-www-form-urlencoded', }, method: 'POST', @@ -106,9 +104,7 @@ function createRefreshRequest(refresh_token) { scope, }, headers: { - authorization: - 'Basic ' + - Buffer.from(client.id + ':' + client.secret).toString('base64'), + authorization: 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), 'content-type': 'application/x-www-form-urlencoded', }, }); diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index f1b83a99..00c94291 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -37,9 +37,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `getAuthorizationCode()`', - ); + e.message.should.equal('Invalid argument: model does not implement `getAuthorizationCode()`'); } }); @@ -54,9 +52,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `revokeAuthorizationCode()`', - ); + e.message.should.equal('Invalid argument: model does not implement `revokeAuthorizationCode()`'); } }); @@ -72,9 +68,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `saveToken()`', - ); + e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); } }); }); @@ -129,9 +123,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - 'Server error: `getAuthorizationCode()` did not return a `client` object', - ); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); } }); @@ -397,9 +389,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - 'Server error: `getAuthorizationCode()` did not return a `client` object', - ); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `client` object'); } }); @@ -428,9 +418,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - 'Server error: `expiresAt` must be a Date instance', - ); + e.message.should.equal('Server error: `expiresAt` must be a Date instance'); } }); @@ -463,9 +451,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - 'Server error: `getAuthorizationCode()` did not return a `user` object', - ); + e.message.should.equal('Server error: `getAuthorizationCode()` did not return a `user` object'); } }); @@ -570,9 +556,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - 'Invalid grant: `redirect_uri` is not a valid URI', - ); + e.message.should.equal('Invalid grant: `redirect_uri` is not a valid URI'); } }); @@ -633,9 +617,7 @@ describe('AuthorizationCodeGrantType integration', function () { query: {}, }); - grantType - .getAuthorizationCode(request, client) - .should.be.an.instanceOf(Promise); + grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); }); it('should support non-promises', function () { @@ -664,9 +646,7 @@ describe('AuthorizationCodeGrantType integration', function () { query: {}, }); - grantType - .getAuthorizationCode(request, client) - .should.be.an.instanceOf(Promise); + grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); }); }); @@ -703,9 +683,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: `redirect_uri` is not a valid URI', - ); + e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); } }); @@ -828,9 +806,7 @@ describe('AuthorizationCodeGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - 'Invalid grant: authorization code is invalid', - ); + e.message.should.equal('Invalid grant: authorization code is invalid'); } } }); @@ -853,9 +829,7 @@ describe('AuthorizationCodeGrantType integration', function () { accessTokenLifetime: 123, model: model, }); - grantType - .revokeAuthorizationCode(authorizationCode) - .should.be.an.instanceOf(Promise); + grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); it('should support non-promises', function () { @@ -876,9 +850,7 @@ describe('AuthorizationCodeGrantType integration', function () { accessTokenLifetime: 123, model: model, }); - grantType - .revokeAuthorizationCode(authorizationCode) - .should.be.an.instanceOf(Promise); + grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); }); @@ -899,11 +871,7 @@ describe('AuthorizationCodeGrantType integration', function () { _client.should.equal('fallback'); return token; }, - validateScope: function ( - _user = 'fallback', - _client = 'fallback', - _scope = ['fallback'], - ) { + validateScope: function (_user = 'fallback', _client = 'fallback', _scope = ['fallback']) { _user.should.equal('fallback'); _client.should.equal('fallback'); _scope.should.eql(['fallback']); diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index da548fd5..af9bf945 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -35,9 +35,7 @@ describe('ClientCredentialsGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `getUserFromClient()`', - ); + e.message.should.equal('Invalid argument: model does not implement `getUserFromClient()`'); } }); @@ -52,9 +50,7 @@ describe('ClientCredentialsGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `saveToken()`', - ); + e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); } }); }); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index 15673112..f6ba8329 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -36,9 +36,7 @@ describe('PasswordGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `getUser()`', - ); + e.message.should.equal('Invalid argument: model does not implement `getUser()`'); } }); @@ -53,9 +51,7 @@ describe('PasswordGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `saveToken()`', - ); + e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); } }); }); @@ -427,11 +423,7 @@ describe('PasswordGrantType integration', function () { const token = {}; const model = Model.from({ getUser: () => should.fail(), - saveToken: async function ( - _token, - _client = 'fallback', - _user = 'fallback', - ) { + saveToken: async function (_token, _client = 'fallback', _user = 'fallback') { _token.accessToken.should.be.a.sha256(); _token.accessTokenExpiresAt.should.be.instanceOf(Date); _token.refreshTokenExpiresAt.should.be.instanceOf(Date); diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index 6c64d79a..d44de133 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -38,9 +38,7 @@ describe('RefreshTokenGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `getRefreshToken()`', - ); + e.message.should.equal('Invalid argument: model does not implement `getRefreshToken()`'); } }); @@ -55,9 +53,7 @@ describe('RefreshTokenGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `revokeToken()`', - ); + e.message.should.equal('Invalid argument: model does not implement `revokeToken()`'); } }); @@ -73,9 +69,7 @@ describe('RefreshTokenGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `saveToken()`', - ); + e.message.should.equal('Invalid argument: model does not implement `saveToken()`'); } }); }); @@ -368,9 +362,7 @@ describe('RefreshTokenGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - 'Server error: `getRefreshToken()` did not return a `client` object', - ); + e.message.should.equal('Server error: `getRefreshToken()` did not return a `client` object'); } }); @@ -400,9 +392,7 @@ describe('RefreshTokenGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - 'Server error: `getRefreshToken()` did not return a `user` object', - ); + e.message.should.equal('Server error: `getRefreshToken()` did not return a `user` object'); } }); @@ -432,9 +422,7 @@ describe('RefreshTokenGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - 'Invalid grant: refresh token was issued to another client', - ); + e.message.should.equal('Invalid grant: refresh token was issued to another client'); } }); @@ -494,9 +482,7 @@ describe('RefreshTokenGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - 'Invalid grant: refresh token was issued to another client', - ); + e.message.should.equal('Invalid grant: refresh token was issued to another client'); } }); @@ -567,9 +553,7 @@ describe('RefreshTokenGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - 'Server error: `refreshTokenExpiresAt` must be a Date instance', - ); + e.message.should.equal('Server error: `refreshTokenExpiresAt` must be a Date instance'); } }); @@ -635,9 +619,7 @@ describe('RefreshTokenGrantType integration', function () { query: {}, }); - grantType - .getRefreshToken(request, client) - .should.be.an.instanceOf(Promise); + grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); }); it('should support non-promises', function () { @@ -661,9 +643,7 @@ describe('RefreshTokenGrantType integration', function () { query: {}, }); - grantType - .getRefreshToken(request, client) - .should.be.an.instanceOf(Promise); + grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); }); }); @@ -684,9 +664,7 @@ describe('RefreshTokenGrantType integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidGrantError); - e.message.should.equal( - 'Invalid grant: refresh token is invalid or could not be revoked', - ); + e.message.should.equal('Invalid grant: refresh token is invalid or could not be revoked'); } }); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index b67b41bf..f3e5e92d 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -41,9 +41,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `getAccessToken()`', - ); + e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); } }); @@ -72,9 +70,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Missing parameter: `addAuthorizedScopesHeader`', - ); + e.message.should.equal('Missing parameter: `addAuthorizedScopesHeader`'); } }); @@ -90,9 +86,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `verifyScope()`', - ); + e.message.should.equal('Invalid argument: model does not implement `verifyScope()`'); } }); @@ -134,9 +128,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: `request` must be an instance of Request', - ); + e.message.should.equal('Invalid argument: `request` must be an instance of Request'); } } }); @@ -161,9 +153,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: `response` must be an instance of Response', - ); + e.message.should.equal('Invalid argument: `response` must be an instance of Response'); } } }); @@ -210,9 +200,7 @@ describe('AuthenticateHandler integration', function () { .handle(request, response) .then(should.fail) .catch(function () { - response - .get('WWW-Authenticate') - .should.equal('Bearer realm="Service",error="invalid_request"'); + response.get('WWW-Authenticate').should.equal('Bearer realm="Service",error="invalid_request"'); }); }); @@ -235,9 +223,7 @@ describe('AuthenticateHandler integration', function () { .handle(request, response) .then(should.fail) .catch(function () { - response - .get('WWW-Authenticate') - .should.equal('Bearer realm="Service",error="invalid_token"'); + response.get('WWW-Authenticate').should.equal('Bearer realm="Service",error="invalid_token"'); }); }); @@ -260,9 +246,7 @@ describe('AuthenticateHandler integration', function () { .handle(request, response) .then(should.fail) .catch(function () { - response - .get('WWW-Authenticate') - .should.equal('Bearer realm="Service",error="insufficient_scope"'); + response.get('WWW-Authenticate').should.equal('Bearer realm="Service",error="insufficient_scope"'); }); }); @@ -403,9 +387,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: only one authentication method is allowed', - ); + e.message.should.equal('Invalid request: only one authentication method is allowed'); } }); @@ -451,9 +433,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: malformed authorization header', - ); + e.message.should.equal('Invalid request: malformed authorization header'); } }); @@ -488,9 +468,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: do not send bearer tokens in query URLs', - ); + e.message.should.equal('Invalid request: do not send bearer tokens in query URLs'); } }); @@ -500,9 +478,7 @@ describe('AuthenticateHandler integration', function () { model: { getAccessToken: function () {} }, }); - handler - .getTokenFromRequestQuery({ query: { access_token: 'foo' } }) - .should.equal('foo'); + handler.getTokenFromRequestQuery({ query: { access_token: 'foo' } }).should.equal('foo'); }); }); @@ -524,9 +500,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: token may not be passed in the body when using the GET verb', - ); + e.message.should.equal('Invalid request: token may not be passed in the body when using the GET verb'); } }); @@ -547,9 +521,7 @@ describe('AuthenticateHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: content must be application/x-www-form-urlencoded', - ); + e.message.should.equal('Invalid request: content must be application/x-www-form-urlencoded'); } }); @@ -600,9 +572,7 @@ describe('AuthenticateHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - 'Server error: `getAccessToken()` did not return a `user` object', - ); + e.message.should.equal('Server error: `getAccessToken()` did not return a `user` object'); }); }); @@ -696,9 +666,7 @@ describe('AuthenticateHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(InsufficientScopeError); - e.message.should.equal( - 'Insufficient scope: authorized scope is insufficient', - ); + e.message.should.equal('Insufficient scope: authorized scope is insufficient'); }); }); @@ -721,9 +689,7 @@ describe('AuthenticateHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(InsufficientScopeError); - e.message.should.equal( - 'Insufficient scope: authorized scope is insufficient', - ); + e.message.should.equal('Insufficient scope: authorized scope is insufficient'); }); }); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 8e5b67c3..2f3a9655 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -42,9 +42,7 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Missing parameter: `authorizationCodeLifetime`', - ); + e.message.should.equal('Missing parameter: `authorizationCodeLifetime`'); } }); @@ -64,9 +62,7 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `getClient()`', - ); + e.message.should.equal('Invalid argument: model does not implement `getClient()`'); } }); @@ -79,9 +75,7 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `saveAuthorizationCode()`', - ); + e.message.should.equal('Invalid argument: model does not implement `saveAuthorizationCode()`'); } }); @@ -97,9 +91,7 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `getAccessToken()`', - ); + e.message.should.equal('Invalid argument: model does not implement `getAccessToken()`'); } }); @@ -126,9 +118,7 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: authenticateHandler does not implement `handle()`', - ); + e.message.should.equal('Invalid argument: authenticateHandler does not implement `handle()`'); } }); @@ -154,12 +144,8 @@ describe('AuthorizeHandler integration', function () { authorizationCodeLifetime: 120, model, }); - handler.authenticateHandler.should.be.an.instanceOf( - CustomAuthenticateHandler, - ); - handler.authenticateHandler.should.not.be.an.instanceOf( - AuthenticateHandler, - ); + handler.authenticateHandler.should.be.an.instanceOf(CustomAuthenticateHandler); + handler.authenticateHandler.should.not.be.an.instanceOf(AuthenticateHandler); }); it('should set the `model`', function () { @@ -185,9 +171,7 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: `request` must be an instance of Request', - ); + e.message.should.equal('Invalid argument: `request` must be an instance of Request'); } }); @@ -209,9 +193,7 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: `response` must be an instance of Response', - ); + e.message.should.equal('Invalid argument: `response` must be an instance of Response'); } }); @@ -260,13 +242,11 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(AccessDeniedError); - e.message.should.equal( - 'Access denied: user denied access to application', - ); + e.message.should.equal('Access denied: user denied access to application'); response .get('location') .should.equal( - 'http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar', + 'http://example.com/cb?error=access_denied&error_description=Access%20denied%3A%20user%20denied%20access%20to%20application&state=foobar' ); } }); @@ -318,7 +298,7 @@ describe('AuthorizeHandler integration', function () { response .get('location') .should.equal( - 'http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar', + 'http://example.com/cb?error=server_error&error_description=Unhandled%20exception&state=foobar' ); } }); @@ -369,7 +349,7 @@ describe('AuthorizeHandler integration', function () { response .get('location') .should.equal( - 'http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar', + 'http://example.com/cb?error=access_denied&error_description=Cannot%20request%20this%20auth%20code&state=foobar' ); } }); @@ -425,9 +405,7 @@ describe('AuthorizeHandler integration', function () { response.status.should.equal(302); response .get('location') - .should.equal( - 'http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo', - ); + .should.equal('http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo'); }); it('should redirect to an error response if `scope` is invalid', async function () { @@ -478,7 +456,7 @@ describe('AuthorizeHandler integration', function () { response .get('location') .should.equal( - 'http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar', + 'http://example.com/cb?error=invalid_scope&error_description=Invalid%20parameter%3A%20%60scope%60&state=foobar' ); } }); @@ -530,9 +508,7 @@ describe('AuthorizeHandler integration', function () { response.status.should.equal(302); response .get('location') - .should.equal( - 'http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo', - ); + .should.equal('http://example.com/cb?code=fooobar-long-authzcode-%3F&state=foobarbazstatemoo'); }); it('should redirect to an error response if `scope` is insufficient (validateScope)', async function () { @@ -590,7 +566,7 @@ describe('AuthorizeHandler integration', function () { response .get('location') .should.equal( - 'http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar', + 'http://example.com/cb?error=invalid_scope&error_description=Invalid%20scope%3A%20Requested%20scope%20is%20invalid&state=foobar' ); } }); @@ -640,7 +616,7 @@ describe('AuthorizeHandler integration', function () { response .get('location') .should.equal( - 'http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60', + 'http://example.com/cb?error=invalid_request&error_description=Missing%20parameter%3A%20%60state%60' ); } }); @@ -685,14 +661,12 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(UnsupportedResponseTypeError); - e.message.should.equal( - 'Unsupported response type: `response_type` is not supported', - ); + e.message.should.equal('Unsupported response type: `response_type` is not supported'); response.status.should.equal(302); response .get('location') .should.equal( - 'http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar', + 'http://example.com/cb?error=unsupported_response_type&error_description=Unsupported%20response%20type%3A%20%60response_type%60%20is%20not%20supported&state=foobar' ); } }); @@ -821,11 +795,7 @@ describe('AuthorizeHandler integration', function () { data.expiresAt.should.be.instanceOf(Date); data.redirectUri.should.equal(client.redirectUris[0]); response.status.should.equal(302); - response - .get('location') - .should.equal( - 'http://example.com/cb?code=long-authz-code&state=fooobarstatebaz', - ); + response.get('location').should.equal('http://example.com/cb?code=long-authz-code&state=fooobarstatebaz'); }); it('should support a custom `authenticateHandler`', async function () { @@ -990,9 +960,7 @@ describe('AuthorizeHandler integration', function () { model, }); - handler - .validateRedirectUri('http://example.com/a', {}) - .should.be.an.instanceOf(Promise); + handler.validateRedirectUri('http://example.com/a', {}).should.be.an.instanceOf(Promise); }); it('should support non-promises', function () { @@ -1010,9 +978,7 @@ describe('AuthorizeHandler integration', function () { model, }); - handler - .validateRedirectUri('http://example.com/a', {}) - .should.be.an.instanceOf(Promise); + handler.validateRedirectUri('http://example.com/a', {}).should.be.an.instanceOf(Promise); }); }); @@ -1098,9 +1064,7 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: `redirect_uri` is not a valid URI', - ); + e.message.should.equal('Invalid request: `redirect_uri` is not a valid URI'); } }); @@ -1126,9 +1090,7 @@ describe('AuthorizeHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - 'Invalid client: client credentials are invalid', - ); + e.message.should.equal('Invalid client: client credentials are invalid'); }); }); @@ -1184,9 +1146,7 @@ describe('AuthorizeHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(UnauthorizedClientError); - e.message.should.equal( - 'Unauthorized client: `grant_type` is invalid', - ); + e.message.should.equal('Unauthorized client: `grant_type` is invalid'); }); }); @@ -1214,9 +1174,7 @@ describe('AuthorizeHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - 'Invalid client: missing client `redirectUri`', - ); + e.message.should.equal('Invalid client: missing client `redirectUri`'); }); }); @@ -1251,9 +1209,7 @@ describe('AuthorizeHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - 'Invalid client: `redirect_uri` does not match client value', - ); + e.message.should.equal('Invalid client: `redirect_uri` does not match client value'); }); }); @@ -1561,9 +1517,7 @@ describe('AuthorizeHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(ServerError); - e.message.should.equal( - 'Server error: `handle()` did not return a `user` object', - ); + e.message.should.equal('Server error: `handle()` did not return a `user` object'); }); }); @@ -1636,9 +1590,7 @@ describe('AuthorizeHandler integration', function () { model, }); - handler - .saveAuthorizationCode('foo', 'bar', 'biz', 'baz') - .should.be.an.instanceOf(Promise); + handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); }); it('should support non-promises when calling `model.saveAuthorizationCode()`', function () { @@ -1654,9 +1606,7 @@ describe('AuthorizeHandler integration', function () { model, }); - handler - .saveAuthorizationCode('foo', 'bar', 'biz', 'baz') - .should.be.an.instanceOf(Promise); + handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); }); }); @@ -1711,9 +1661,7 @@ describe('AuthorizeHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(UnsupportedResponseTypeError); - e.message.should.equal( - 'Unsupported response type: `response_type` is not supported', - ); + e.message.should.equal('Unsupported response type: `response_type` is not supported'); } }); @@ -1776,10 +1724,7 @@ describe('AuthorizeHandler integration', function () { model, }); const responseType = new CodeResponseType(12345); - const redirectUri = handler.buildSuccessRedirectUri( - 'http://example.com/cb', - responseType, - ); + const redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); url.format(redirectUri).should.equal('http://example.com/cb?code=12345'); }); @@ -1797,16 +1742,9 @@ describe('AuthorizeHandler integration', function () { authorizationCodeLifetime: 120, model, }); - const redirectUri = handler.buildErrorRedirectUri( - 'http://example.com/cb', - error, - ); + const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); - url - .format(redirectUri) - .should.equal( - 'http://example.com/cb?error=invalid_client&error_description=foo%20bar', - ); + url.format(redirectUri).should.equal('http://example.com/cb?error=invalid_client&error_description=foo%20bar'); }); it('should return a redirect uri', function () { @@ -1820,16 +1758,11 @@ describe('AuthorizeHandler integration', function () { authorizationCodeLifetime: 120, model, }); - const redirectUri = handler.buildErrorRedirectUri( - 'http://example.com/cb', - error, - ); + const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); url .format(redirectUri) - .should.equal( - 'http://example.com/cb?error=invalid_client&error_description=Bad%20Request', - ); + .should.equal('http://example.com/cb?error=invalid_client&error_description=Bad%20Request'); }); }); @@ -1849,9 +1782,7 @@ describe('AuthorizeHandler integration', function () { handler.updateResponse(response, uri, 'foobar'); - response - .get('location') - .should.equal('http://example.com/cb?state=foobar'); + response.get('location').should.equal('http://example.com/cb?state=foobar'); }); }); @@ -1882,14 +1813,10 @@ describe('AuthorizeHandler integration', function () { } catch (e) { if (method === 'plain') { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"', - ); + e.message.should.equal('Invalid request: `code_challenge_method` "plain" is not allowed; use "S256"'); } else { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - `Invalid request: transform algorithm '${method}' not supported`, - ); + e.message.should.equal(`Invalid request: transform algorithm '${method}' not supported`); } } } @@ -1940,9 +1867,7 @@ describe('AuthorizeHandler integration', function () { } catch (e) { // defined in RFC 7636 - 4.4 e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - "Invalid request: transform algorithm 'foo' not supported", - ); + e.message.should.equal("Invalid request: transform algorithm 'foo' not supported"); } }); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index af1fe572..010a9292 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -73,9 +73,7 @@ describe('TokenHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: model does not implement `getClient()`', - ); + e.message.should.equal('Invalid argument: model does not implement `getClient()`'); } }); @@ -107,9 +105,7 @@ describe('TokenHandler integration', function () { alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, }); - handler.alwaysIssueNewRefreshToken.should.equal( - alwaysIssueNewRefreshToken, - ); + handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); }); it('should set the `alwaysIssueNewRefreshToken` to false', function () { @@ -125,9 +121,7 @@ describe('TokenHandler integration', function () { alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, }); - handler.alwaysIssueNewRefreshToken.should.equal( - alwaysIssueNewRefreshToken, - ); + handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); }); it('should return the default `alwaysIssueNewRefreshToken` value', function () { @@ -207,9 +201,7 @@ describe('TokenHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: `request` must be an instance of Request', - ); + e.message.should.equal('Invalid argument: `request` must be an instance of Request'); } }); @@ -236,9 +228,7 @@ describe('TokenHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidArgumentError); - e.message.should.equal( - 'Invalid argument: `response` must be an instance of Response', - ); + e.message.should.equal('Invalid argument: `response` must be an instance of Response'); } }); @@ -292,9 +282,7 @@ describe('TokenHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: content must be application/x-www-form-urlencoded', - ); + e.message.should.equal('Invalid request: content must be application/x-www-form-urlencoded'); }); }); @@ -324,9 +312,7 @@ describe('TokenHandler integration', function () { .then(should.fail) .catch(function (e) { e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - 'Invalid client: cannot retrieve client credentials', - ); + e.message.should.equal('Invalid client: cannot retrieve client credentials'); }); }); @@ -733,10 +719,7 @@ describe('TokenHandler integration', function () { const request = new Request({ body: {}, headers: { - authorization: util.format( - 'Basic %s', - Buffer.from('foo:bar').toString('base64'), - ), + authorization: util.format('Basic %s', Buffer.from('foo:bar').toString('base64')), }, method: {}, query: {}, @@ -751,9 +734,7 @@ describe('TokenHandler integration', function () { e.code.should.equal(401); e.message.should.equal('Invalid client: client is invalid'); - response - .get('WWW-Authenticate') - .should.equal('Basic realm="Service"'); + response.get('WWW-Authenticate').should.equal('Basic realm="Service"'); }); }); @@ -840,10 +821,7 @@ describe('TokenHandler integration', function () { const request = new Request({ body: { grant_type: 'password' }, headers: { - authorization: util.format( - 'Basic %s', - Buffer.from('blah:').toString('base64'), - ), + authorization: util.format('Basic %s', Buffer.from('blah:').toString('base64')), }, method: {}, query: {}, @@ -927,9 +905,7 @@ describe('TokenHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - 'Invalid client: cannot retrieve client credentials', - ); + e.message.should.equal('Invalid client: cannot retrieve client credentials'); } }); @@ -956,9 +932,7 @@ describe('TokenHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(InvalidClientError); - e.message.should.equal( - 'Invalid client: cannot retrieve client credentials', - ); + e.message.should.equal('Invalid client: cannot retrieve client credentials'); } }); @@ -1000,10 +974,7 @@ describe('TokenHandler integration', function () { const request = new Request({ body: {}, headers: { - authorization: util.format( - 'Basic %s', - Buffer.from('foo:bar').toString('base64'), - ), + authorization: util.format('Basic %s', Buffer.from('foo:bar').toString('base64')), }, method: {}, query: {}, @@ -1116,9 +1087,7 @@ describe('TokenHandler integration', function () { should.fail(); } catch (e) { e.should.be.an.instanceOf(UnsupportedGrantTypeError); - e.message.should.equal( - 'Unsupported grant type: `grant_type` is invalid', - ); + e.message.should.equal('Unsupported grant type: `grant_type` is invalid'); } }); @@ -1238,18 +1207,14 @@ describe('TokenHandler integration', function () { const methods = ['S256', undefined]; for (const method of methods) { - const codeVerifier = stringUtil.base64URLEncode( - crypto.randomBytes(32), - ); + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: method, - codeChallenge: stringUtil.base64URLEncode( - crypto.createHash('sha256').update(codeVerifier).digest(), - ), + codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()), }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; @@ -1293,9 +1258,7 @@ describe('TokenHandler integration', function () { const methods = ['plain', undefined]; for (const method of methods) { - const codeVerifier = stringUtil.base64URLEncode( - crypto.randomBytes(32), - ); + const codeVerifier = stringUtil.base64URLEncode(crypto.randomBytes(32)); const authorizationCode = { authorizationCode: 12345, client: { id: 'foobar' }, @@ -1351,9 +1314,7 @@ describe('TokenHandler integration', function () { expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: 'S256', - codeChallenge: stringUtil.base64URLEncode( - crypto.createHash('sha256').update(codeVerifier).digest(), - ), + codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()), }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; @@ -1405,9 +1366,7 @@ describe('TokenHandler integration', function () { expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: 'S256', - codeChallenge: stringUtil.base64URLEncode( - crypto.createHash('sha256').update(codeVerifier).digest(), - ), + codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()), }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; diff --git a/test/integration/response-types/code-response-type_test.js b/test/integration/response-types/code-response-type_test.js index 222ce6a5..d97c48e3 100644 --- a/test/integration/response-types/code-response-type_test.js +++ b/test/integration/response-types/code-response-type_test.js @@ -49,22 +49,16 @@ describe('CodeResponseType integration', function () { it('should return the new redirect uri and set the `code` and `state` in the query', function () { const responseType = new CodeResponseType('foo'); - const redirectUri = responseType.buildRedirectUri( - 'http://example.com/cb', - ); + const redirectUri = responseType.buildRedirectUri('http://example.com/cb'); url.format(redirectUri).should.equal('http://example.com/cb?code=foo'); }); it('should return the new redirect uri and append the `code` and `state` in the query', function () { const responseType = new CodeResponseType('foo'); - const redirectUri = responseType.buildRedirectUri( - 'http://example.com/cb?foo=bar', - ); + const redirectUri = responseType.buildRedirectUri('http://example.com/cb?foo=bar'); - url - .format(redirectUri) - .should.equal('http://example.com/cb?foo=bar&code=foo'); + url.format(redirectUri).should.equal('http://example.com/cb?foo=bar&code=foo'); }); }); }); diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js index 946aa1ee..24cc6292 100644 --- a/test/unit/grant-types/abstract-grant-type_test.js +++ b/test/unit/grant-types/abstract-grant-type_test.js @@ -17,9 +17,7 @@ describe('AbstractGrantType', function () { describe('generateAccessToken()', function () { it('should call `model.generateAccessToken()`', function () { const model = Model.from({ - generateAccessToken: sinon - .stub() - .returns({ client: {}, expiresAt: new Date(), user: {} }), + generateAccessToken: sinon.stub().returns({ client: {}, expiresAt: new Date(), user: {} }), }); const handler = new AbstractGrantType({ accessTokenLifetime: 120, diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index 49c15485..e723e58a 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -71,9 +71,7 @@ describe('AuthorizationCodeGrantType', function () { .then(function () { model.revokeAuthorizationCode.callCount.should.equal(1); model.revokeAuthorizationCode.firstCall.args.should.have.length(1); - model.revokeAuthorizationCode.firstCall.args[0].should.equal( - authorizationCode, - ); + model.revokeAuthorizationCode.firstCall.args[0].should.equal(authorizationCode); model.revokeAuthorizationCode.firstCall.thisValue.should.equal(model); }) .catch(should.fail); @@ -95,18 +93,10 @@ describe('AuthorizationCodeGrantType', function () { }); sinon.stub(handler, 'validateScope').returns(['foobiz']); - sinon - .stub(handler, 'generateAccessToken') - .returns(Promise.resolve('foo')); - sinon - .stub(handler, 'generateRefreshToken') - .returns(Promise.resolve('bar')); - sinon - .stub(handler, 'getAccessTokenExpiresAt') - .returns(Promise.resolve('biz')); - sinon - .stub(handler, 'getRefreshTokenExpiresAt') - .returns(Promise.resolve('baz')); + sinon.stub(handler, 'generateAccessToken').returns(Promise.resolve('foo')); + sinon.stub(handler, 'generateRefreshToken').returns(Promise.resolve('bar')); + sinon.stub(handler, 'getAccessTokenExpiresAt').returns(Promise.resolve('biz')); + sinon.stub(handler, 'getRefreshTokenExpiresAt').returns(Promise.resolve('baz')); return handler .saveToken(user, client, 'foobar', ['foobiz']) @@ -141,9 +131,7 @@ describe('AuthorizationCodeGrantType', function () { expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: 'S256', - codeChallenge: stringUtil.base64URLEncode( - crypto.createHash('sha256').update(codeVerifier).digest(), - ), + codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()), }; const client = { id: 'foobar', isPublic: true }; const model = Model.from({ diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js index 68ca43cb..92d2dfc5 100644 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ b/test/unit/grant-types/refresh-token-grant-type_test.js @@ -59,9 +59,7 @@ describe('RefreshTokenGrantType', function () { describe('getRefreshToken()', function () { it('should call `model.getRefreshToken()`', function () { const model = Model.from({ - getRefreshToken: sinon - .stub() - .returns({ accessToken: 'foo', client: {}, user: {} }), + getRefreshToken: sinon.stub().returns({ accessToken: 'foo', client: {}, user: {} }), saveToken: function () {}, revokeToken: function () {}, }); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index decf139a..fa1a38fb 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -38,9 +38,7 @@ describe('AuthenticateHandler', function () { should.fail('should.fail', ''); } catch (e) { e.should.be.an.instanceOf(InvalidRequestError); - e.message.should.equal( - 'Invalid request: malformed authorization header', - ); + e.message.should.equal('Invalid request: malformed authorization header'); } }); }); @@ -62,9 +60,7 @@ describe('AuthenticateHandler', function () { handler.getTokenFromRequest(request); handler.getTokenFromRequestHeader.callCount.should.equal(1); - handler.getTokenFromRequestHeader.firstCall.args[0].should.equal( - request, - ); + handler.getTokenFromRequestHeader.firstCall.args[0].should.equal(request); handler.getTokenFromRequestHeader.restore(); }); }); @@ -86,9 +82,7 @@ describe('AuthenticateHandler', function () { handler.getTokenFromRequest(request); handler.getTokenFromRequestQuery.callCount.should.equal(1); - handler.getTokenFromRequestQuery.firstCall.args[0].should.equal( - request, - ); + handler.getTokenFromRequestQuery.firstCall.args[0].should.equal(request); handler.getTokenFromRequestQuery.restore(); }); }); diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index d733a449..56557c18 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -33,9 +33,7 @@ describe('AuthorizeHandler', function () { .generateAuthorizationCode() .then(function () { model.generateAuthorizationCode.callCount.should.equal(1); - model.generateAuthorizationCode.firstCall.thisValue.should.equal( - model, - ); + model.generateAuthorizationCode.firstCall.thisValue.should.equal(model); }) .catch(should.fail); }); @@ -150,16 +148,7 @@ describe('AuthorizeHandler', function () { }); return handler - .saveAuthorizationCode( - 'foo', - 'bar', - ['qux'], - 'biz', - 'baz', - 'boz', - 'codeChallenge', - 'codeChallengeMethod', - ) + .saveAuthorizationCode('foo', 'bar', ['qux'], 'biz', 'baz', 'boz', 'codeChallenge', 'codeChallengeMethod') .then(function () { model.saveAuthorizationCode.callCount.should.equal(1); model.saveAuthorizationCode.firstCall.args.should.have.length(3); @@ -213,9 +202,7 @@ describe('AuthorizeHandler', function () { model.validateRedirectUri.callCount.should.equal(1); model.validateRedirectUri.firstCall.args.should.have.length(2); - model.validateRedirectUri.firstCall.args[0].should.equal( - redirect_uri, - ); + model.validateRedirectUri.firstCall.args[0].should.equal(redirect_uri); model.validateRedirectUri.firstCall.args[1].should.equal(client); model.validateRedirectUri.firstCall.thisValue.should.equal(model); }) @@ -286,9 +273,7 @@ describe('AuthorizeHandler', function () { }) .catch((err) => { err.name.should.equal('invalid_client'); - err.message.should.equal( - 'Invalid client: `redirect_uri` does not match client value', - ); + err.message.should.equal('Invalid client: `redirect_uri` does not match client value'); }); }); }); diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model_test.js index 2c1f56bd..2ac275df 100644 --- a/test/unit/models/token-model_test.js +++ b/test/unit/models/token-model_test.js @@ -147,14 +147,12 @@ describe('TokenModel', function () { }, { allowExtendedTokenAttributes: true, - }, + } ); should.not.exist(model['myCustomAttribute']); model['customAttributes'].should.be.an('object'); - model['customAttributes']['myCustomAttribute'].should.equal( - 'myCustomValue', - ); + model['customAttributes']['myCustomAttribute'].should.equal('myCustomValue'); }); }); }); diff --git a/test/unit/pkce/pkce_test.js b/test/unit/pkce/pkce_test.js index 4ee90b30..e2461ce3 100644 --- a/test/unit/pkce/pkce_test.js +++ b/test/unit/pkce/pkce_test.js @@ -14,11 +14,7 @@ describe('PKCE', function () { it('returns, whether parameters define a PKCE request', function () { [ [true, 'authorization_code', 'foo'], - [ - true, - 'authorization_code', - '123123123123123123123123123123123123123123123', - ], + [true, 'authorization_code', '123123123123123123123123123123123123123123123'], [false, 'authorization_code', ''], [false, 'authorization_code', undefined], [false, 'foo_code', '123123123123123123123123123123123123123123123'], @@ -31,7 +27,7 @@ describe('PKCE', function () { pkce.isPKCERequest({ grantType: triple[1], codeVerifier: triple[2], - }), + }) ); }); }); @@ -49,10 +45,7 @@ describe('PKCE', function () { '123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123', ], // too long // invalid chars - [ - true, - '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ', - ], + [true, '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'], ].forEach((pair) => { should.equal(pair[0], pkce.codeChallengeMatchesABNF(pair[1])); }); @@ -60,8 +53,7 @@ describe('PKCE', function () { }); describe(pkce.getHashForCodeChallenge.name, function () { it('returns nothing if method is not valid', function () { - const verifier = - '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; + const verifier = '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; [ [undefined, undefined, verifier], @@ -74,7 +66,7 @@ describe('PKCE', function () { pkce.getHashForCodeChallenge({ method: triple[1], verifier: triple[2], - }), + }) ); }); }); @@ -92,19 +84,17 @@ describe('PKCE', function () { pkce.getHashForCodeChallenge({ method: triple[1], verifier: triple[2], - }), + }) ); }); }); it('returns the unhashed verifier when method is plain', function () { - const verifier = - '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; + const verifier = '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; const hash = pkce.getHashForCodeChallenge({ method: 'plain', verifier }); should.equal(hash, verifier); }); it('returns the hash verifier when method is S256', function () { - const verifier = - '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; + const verifier = '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'; const hash = pkce.getHashForCodeChallenge({ method: 'S256', verifier }); const expectedHash = base64URLEncode(createHash({ data: verifier })); should.equal(hash, expectedHash); diff --git a/test/unit/request_test.js b/test/unit/request_test.js index ed121b02..4b5aa8bc 100644 --- a/test/unit/request_test.js +++ b/test/unit/request_test.js @@ -34,11 +34,7 @@ describe('Request', function () { [null, TypeError, "Cannot destructure property 'headers'"], [{}, InvalidArgumentError, 'Missing parameter: `headers`'], [{ headers: {} }, InvalidArgumentError, 'Missing parameter: `method`'], - [ - { headers: {}, method: 'GET' }, - InvalidArgumentError, - 'Missing parameter: `query`', - ], + [{ headers: {}, method: 'GET' }, InvalidArgumentError, 'Missing parameter: `query`'], ]; args.forEach(([value, error, message]) => { @@ -189,25 +185,17 @@ describe('Request', function () { it('should validate the content-type', function () { const originalRequest = generateBaseRequest(); - originalRequest.headers['content-type'] = - 'application/x-www-form-urlencoded'; - originalRequest.headers['content-length'] = JSON.stringify( - originalRequest.body, - ).length; + originalRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; + originalRequest.headers['content-length'] = JSON.stringify(originalRequest.body).length; const request = new Request(originalRequest); - request - .is('application/x-www-form-urlencoded') - .should.eql('application/x-www-form-urlencoded'); + request.is('application/x-www-form-urlencoded').should.eql('application/x-www-form-urlencoded'); }); it('should return false if the content-type is invalid', function () { const originalRequest = generateBaseRequest(); - originalRequest.headers['content-type'] = - 'application/x-www-form-urlencoded'; - originalRequest.headers['content-length'] = JSON.stringify( - originalRequest.body, - ).length; + originalRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; + originalRequest.headers['content-length'] = JSON.stringify(originalRequest.body).length; const request = new Request(originalRequest); request.is('application/json').should.eql(false); diff --git a/test/unit/server_test.js b/test/unit/server_test.js index 379bb477..c9b1b7fd 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -23,16 +23,12 @@ describe('Server', function () { }); const server = new Server({ model: model }); - sinon - .stub(AuthenticateHandler.prototype, 'handle') - .returns(Promise.resolve()); + sinon.stub(AuthenticateHandler.prototype, 'handle').returns(Promise.resolve()); server.authenticate('foo'); AuthenticateHandler.prototype.handle.callCount.should.equal(1); - AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal( - 'foo', - ); + AuthenticateHandler.prototype.handle.firstCall.args[0].should.equal('foo'); AuthenticateHandler.prototype.handle.restore(); }); }); @@ -46,9 +42,7 @@ describe('Server', function () { }); const server = new Server({ model: model }); - sinon - .stub(AuthorizeHandler.prototype, 'handle') - .returns(Promise.resolve()); + sinon.stub(AuthorizeHandler.prototype, 'handle').returns(Promise.resolve()); server.authorize('foo', 'bar'); diff --git a/test/unit/utils/crypto-util_test.js b/test/unit/utils/crypto-util_test.js index 00e8f618..b25edf2d 100644 --- a/test/unit/utils/crypto-util_test.js +++ b/test/unit/utils/crypto-util_test.js @@ -5,9 +5,7 @@ describe(cryptoUtil.createHash.name, function () { it('creates a hash by given algorithm', function () { const data = 'client-credentials-grant'; const hash = cryptoUtil.createHash({ data, output: 'hex' }); - hash.should.equal( - '072726830f0aadd2d91f86f53e3a7ef40018c2626438152dd576e272bf2b8e60', - ); + hash.should.equal('072726830f0aadd2d91f86f53e3a7ef40018c2626438152dd576e272bf2b8e60'); }); it('should throw if data is missing', function () { try { @@ -15,7 +13,7 @@ describe(cryptoUtil.createHash.name, function () { } catch (e) { e.should.be.instanceOf(TypeError); e.message.should.include( - 'he "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.', + 'he "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.' ); } }); diff --git a/test/unit/utils/scope-util_test.js b/test/unit/utils/scope-util_test.js index 2cc8be3a..a25358c2 100644 --- a/test/unit/utils/scope-util_test.js +++ b/test/unit/utils/scope-util_test.js @@ -10,17 +10,7 @@ describe(parseScope.name, () => { }); }); it('should throw on non-string values', () => { - const invalid = [ - 1, - -1, - true, - false, - {}, - ['foo'], - [], - () => {}, - Symbol('foo'), - ]; + const invalid = [1, -1, true, false, {}, ['foo'], [], () => {}, Symbol('foo')]; invalid.forEach((str) => { try { parseScope(str); From b54396bb45a39d15f38a9bb3468d2d570edbe8cf Mon Sep 17 00:00:00 2001 From: jankapunkt Date: Wed, 17 Jun 2026 09:11:06 +0200 Subject: [PATCH 11/11] fix(core): no trailing commas in array --- biome.json | 2 +- lib/errors/access-denied-error.js | 2 +- lib/errors/insufficient-scope-error.js | 2 +- lib/errors/invalid-argument-error.js | 2 +- lib/errors/invalid-client-error.js | 2 +- lib/errors/invalid-grant-error.js | 2 +- lib/errors/invalid-request-error.js | 2 +- lib/errors/invalid-scope-error.js | 2 +- lib/errors/invalid-token-error.js | 2 +- lib/errors/server-error.js | 2 +- lib/errors/unauthorized-client-error.js | 2 +- lib/errors/unauthorized-request-error.js | 2 +- lib/errors/unsupported-grant-type-error.js | 2 +- lib/errors/unsupported-response-type-error.js | 2 +- .../authorization-code-grant-type.js | 4 +- .../client-credentials-grant-type.js | 2 +- lib/grant-types/password-grant-type.js | 2 +- lib/grant-types/refresh-token-grant-type.js | 2 +- lib/handlers/authorize-handler.js | 8 +- lib/handlers/token-handler.js | 10 +- lib/models/token-model.js | 2 +- lib/pkce/pkce.js | 2 +- lib/server.js | 6 +- lib/token-types/bearer-token-type.js | 2 +- lib/utils/date-util.js | 2 +- lib/utils/token-util.js | 2 +- test/compliance/client-authentication_test.js | 8 +- .../client-credential-workflow_test.js | 18 +- test/compliance/password-grant-type_test.js | 20 +- test/compliance/pkce_test.js | 28 +- .../refresh-token-grant-type_test.js | 16 +- test/helpers/model.js | 8 +- test/helpers/request.js | 2 +- .../grant-types/abstract-grant-type_test.js | 52 +- .../authorization-code-grant-type_test.js | 192 +++---- .../client-credentials-grant-type_test.js | 66 +-- .../grant-types/password-grant-type_test.js | 90 ++-- .../refresh-token-grant-type_test.js | 158 +++--- .../handlers/authenticate-handler_test.js | 158 +++--- .../handlers/authorize-handler_test.js | 478 +++++++++--------- .../handlers/token-handler_test.js | 402 +++++++-------- test/integration/request_test.js | 32 +- test/integration/response_test.js | 4 +- test/integration/server_test.js | 48 +- .../token-types/bearer-token-type_test.js | 8 +- .../grant-types/abstract-grant-type_test.js | 10 +- .../authorization-code-grant-type_test.js | 34 +- .../client-credentials-grant-type_test.js | 10 +- .../grant-types/password-grant-type_test.js | 12 +- .../refresh-token-grant-type_test.js | 50 +- .../handlers/authenticate-handler_test.js | 32 +- test/unit/handlers/authorize-handler_test.js | 56 +- test/unit/handlers/token-handler_test.js | 6 +- test/unit/models/token-model_test.js | 18 +- test/unit/pkce/pkce_test.js | 16 +- test/unit/request_test.js | 24 +- test/unit/response_test.js | 16 +- test/unit/server_test.js | 6 +- test/unit/utils/scope-util_test.js | 2 +- 59 files changed, 1076 insertions(+), 1076 deletions(-) diff --git a/biome.json b/biome.json index 907c4160..081db071 100644 --- a/biome.json +++ b/biome.json @@ -44,7 +44,7 @@ "formatter": { "quoteStyle": "single", "indentStyle": "space", - "trailingCommas": "es5", + "trailingCommas": "none", "lineWidth": 120 } }, diff --git a/lib/errors/access-denied-error.js b/lib/errors/access-denied-error.js index d2c510cf..a573ebbd 100644 --- a/lib/errors/access-denied-error.js +++ b/lib/errors/access-denied-error.js @@ -21,7 +21,7 @@ class AccessDeniedError extends OAuthError { properties = { code: 400, name: 'access_denied', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/insufficient-scope-error.js b/lib/errors/insufficient-scope-error.js index 82cf3bb3..3ff9bc48 100644 --- a/lib/errors/insufficient-scope-error.js +++ b/lib/errors/insufficient-scope-error.js @@ -22,7 +22,7 @@ class InsufficientScopeError extends OAuthError { properties = { code: 403, name: 'insufficient_scope', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/invalid-argument-error.js b/lib/errors/invalid-argument-error.js index 3a1a4002..9c68b13d 100644 --- a/lib/errors/invalid-argument-error.js +++ b/lib/errors/invalid-argument-error.js @@ -21,7 +21,7 @@ class InvalidArgumentError extends OAuthError { properties = { code: 500, name: 'invalid_argument', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/invalid-client-error.js b/lib/errors/invalid-client-error.js index 2c18fabe..7fe53146 100644 --- a/lib/errors/invalid-client-error.js +++ b/lib/errors/invalid-client-error.js @@ -23,7 +23,7 @@ class InvalidClientError extends OAuthError { properties = { code: 400, name: 'invalid_client', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/invalid-grant-error.js b/lib/errors/invalid-grant-error.js index 10e84693..171b39c4 100644 --- a/lib/errors/invalid-grant-error.js +++ b/lib/errors/invalid-grant-error.js @@ -24,7 +24,7 @@ class InvalidGrantError extends OAuthError { properties = { code: 400, name: 'invalid_grant', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/invalid-request-error.js b/lib/errors/invalid-request-error.js index 15f87b56..6692616e 100644 --- a/lib/errors/invalid-request-error.js +++ b/lib/errors/invalid-request-error.js @@ -20,7 +20,7 @@ class InvalidRequest extends OAuthError { properties = { code: 400, name: 'invalid_request', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/invalid-scope-error.js b/lib/errors/invalid-scope-error.js index 38aec9a1..83098dcb 100644 --- a/lib/errors/invalid-scope-error.js +++ b/lib/errors/invalid-scope-error.js @@ -19,7 +19,7 @@ class InvalidScopeError extends OAuthError { properties = { code: 400, name: 'invalid_scope', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/invalid-token-error.js b/lib/errors/invalid-token-error.js index 96469e44..a3c0ff27 100644 --- a/lib/errors/invalid-token-error.js +++ b/lib/errors/invalid-token-error.js @@ -19,7 +19,7 @@ class InvalidTokenError extends OAuthError { properties = { code: 401, name: 'invalid_token', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/server-error.js b/lib/errors/server-error.js index 0a87eaf1..04202097 100644 --- a/lib/errors/server-error.js +++ b/lib/errors/server-error.js @@ -23,7 +23,7 @@ class ServerError extends OAuthError { properties = { code: 503, name: 'server_error', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/unauthorized-client-error.js b/lib/errors/unauthorized-client-error.js index a175ad69..243c1acc 100644 --- a/lib/errors/unauthorized-client-error.js +++ b/lib/errors/unauthorized-client-error.js @@ -19,7 +19,7 @@ class UnauthorizedClientError extends OAuthError { properties = { code: 400, name: 'unauthorized_client', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/unauthorized-request-error.js b/lib/errors/unauthorized-request-error.js index ae4e2dcb..a3f5ec63 100644 --- a/lib/errors/unauthorized-request-error.js +++ b/lib/errors/unauthorized-request-error.js @@ -22,7 +22,7 @@ class UnauthorizedRequestError extends OAuthError { properties = { code: 401, name: 'unauthorized_request', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/unsupported-grant-type-error.js b/lib/errors/unsupported-grant-type-error.js index 961641af..d15fd64c 100644 --- a/lib/errors/unsupported-grant-type-error.js +++ b/lib/errors/unsupported-grant-type-error.js @@ -19,7 +19,7 @@ class UnsupportedGrantTypeError extends OAuthError { properties = { code: 400, name: 'unsupported_grant_type', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/errors/unsupported-response-type-error.js b/lib/errors/unsupported-response-type-error.js index 3ecfa596..c4b68fb5 100644 --- a/lib/errors/unsupported-response-type-error.js +++ b/lib/errors/unsupported-response-type-error.js @@ -20,7 +20,7 @@ class UnsupportedResponseTypeError extends OAuthError { properties = { code: 400, name: 'unsupported_response_type', - ...properties, + ...properties }; super(message, properties); diff --git a/lib/grant-types/authorization-code-grant-type.js b/lib/grant-types/authorization-code-grant-type.js index ff336202..810ba257 100644 --- a/lib/grant-types/authorization-code-grant-type.js +++ b/lib/grant-types/authorization-code-grant-type.js @@ -151,7 +151,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType { const hash = pkce.getHashForCodeChallenge({ verifier: request.body.code_verifier, - method, + method }); if (!hash) { @@ -260,7 +260,7 @@ class AuthorizationCodeGrantType extends AbstractGrantType { accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, - scope: validatedScope, + scope: validatedScope }; return this.model.saveToken(token, client, user); diff --git a/lib/grant-types/client-credentials-grant-type.js b/lib/grant-types/client-credentials-grant-type.js index c2874f0b..604d2dfc 100644 --- a/lib/grant-types/client-credentials-grant-type.js +++ b/lib/grant-types/client-credentials-grant-type.js @@ -75,7 +75,7 @@ class ClientCredentialsGrantType extends AbstractGrantType { const token = { accessToken, accessTokenExpiresAt, - scope: validatedScope, + scope: validatedScope }; return this.model.saveToken(token, client, user); diff --git a/lib/grant-types/password-grant-type.js b/lib/grant-types/password-grant-type.js index 2c0128a7..b2dee312 100644 --- a/lib/grant-types/password-grant-type.js +++ b/lib/grant-types/password-grant-type.js @@ -99,7 +99,7 @@ class PasswordGrantType extends AbstractGrantType { accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, - scope: validatedScope, + scope: validatedScope }; return this.model.saveToken(token, client, user); diff --git a/lib/grant-types/refresh-token-grant-type.js b/lib/grant-types/refresh-token-grant-type.js index 596a7070..26ac3309 100644 --- a/lib/grant-types/refresh-token-grant-type.js +++ b/lib/grant-types/refresh-token-grant-type.js @@ -137,7 +137,7 @@ class RefreshTokenGrantType extends AbstractGrantType { const token = { accessToken, accessTokenExpiresAt, - scope, + scope }; if (this.alwaysIssueNewRefreshToken !== false) { diff --git a/lib/handlers/authorize-handler.js b/lib/handlers/authorize-handler.js index bdba9831..34e3dcc6 100644 --- a/lib/handlers/authorize-handler.js +++ b/lib/handlers/authorize-handler.js @@ -27,7 +27,7 @@ const { parseScope } = require('../utils/scope-util'); */ const responseTypes = { - code: require('../response-types/code-response-type'), + code: require('../response-types/code-response-type') //token: require('../response-types/token-response-type') }; @@ -293,14 +293,14 @@ class AuthorizeHandler { authorizationCode: authorizationCode, expiresAt: expiresAt, redirectUri: redirectUri, - scope: scope, + scope: scope }; if (codeChallenge && codeChallengeMethod) { code = Object.assign( { codeChallenge: codeChallenge, - codeChallengeMethod: codeChallengeMethod, + codeChallengeMethod: codeChallengeMethod }, code ); @@ -350,7 +350,7 @@ class AuthorizeHandler { const uri = url.parse(redirectUri); uri.query = { - error: error.name, + error: error.name }; if (error.message) { diff --git a/lib/handlers/token-handler.js b/lib/handlers/token-handler.js index 8eac7aac..d09fc256 100644 --- a/lib/handlers/token-handler.js +++ b/lib/handlers/token-handler.js @@ -27,7 +27,7 @@ const grantTypes = { authorization_code: require('../grant-types/authorization-code-grant-type'), client_credentials: require('../grant-types/client-credentials-grant-type'), password: require('../grant-types/password-grant-type'), - refresh_token: require('../grant-types/refresh-token-grant-type'), + refresh_token: require('../grant-types/refresh-token-grant-type') }; /** @@ -89,7 +89,7 @@ class TokenHandler { const client = await this.getClient(request, response); const data = await this.handleGrantType(request, client); const model = new TokenModel(data, { - allowExtendedTokenAttributes: this.allowExtendedTokenAttributes, + allowExtendedTokenAttributes: this.allowExtendedTokenAttributes }); const tokenType = this.getTokenType(model); @@ -185,7 +185,7 @@ class TokenHandler { if (request.body.client_id && request.body.client_secret) { return { clientId: request.body.client_id, - clientSecret: request.body.client_secret, + clientSecret: request.body.client_secret }; } @@ -236,7 +236,7 @@ class TokenHandler { model: this.model, refreshTokenLifetime: refreshTokenLifetime, alwaysIssueNewRefreshToken: this.alwaysIssueNewRefreshToken, - enablePlainPKCE: this.enablePlainPKCE === true, + enablePlainPKCE: this.enablePlainPKCE === true }; return new Type(options).handle(request, client); @@ -296,7 +296,7 @@ class TokenHandler { updateErrorResponse(response, error) { response.body = { error: error.name, - error_description: error.message, + error_description: error.message }; response.status = error.code; diff --git a/lib/models/token-model.js b/lib/models/token-model.js index 12dd3bb5..5bb4af7d 100644 --- a/lib/models/token-model.js +++ b/lib/models/token-model.js @@ -19,7 +19,7 @@ const modelAttributes = new Set([ 'refreshTokenExpiresAt', 'scope', 'client', - 'user', + 'user' ]); /** diff --git a/lib/pkce/pkce.js b/lib/pkce/pkce.js index 303d0123..fc3941a4 100644 --- a/lib/pkce/pkce.js +++ b/lib/pkce/pkce.js @@ -87,5 +87,5 @@ module.exports = { getHashForCodeChallenge, codeChallengeMatchesABNF, isPKCERequest, - isValidMethod, + isValidMethod }; diff --git a/lib/server.js b/lib/server.js index 6d52c1d5..10af1a8d 100644 --- a/lib/server.js +++ b/lib/server.js @@ -102,7 +102,7 @@ class OAuth2Server { { addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, - allowBearerTokensInQueryString: false, + allowBearerTokensInQueryString: false }, this.options, options @@ -170,7 +170,7 @@ class OAuth2Server { options = Object.assign( { allowEmptyState: false, - authorizationCodeLifetime: 5 * 60, // 5 minutes. + authorizationCodeLifetime: 5 * 60 // 5 minutes. }, this.options, options @@ -237,7 +237,7 @@ class OAuth2Server { accessTokenLifetime: 60 * 60, // 1 hour. refreshTokenLifetime: 60 * 60 * 24 * 14, // 2 weeks. allowExtendedTokenAttributes: false, - requireClientAuthentication: {}, // defaults to true for all grant types + requireClientAuthentication: {} // defaults to true for all grant types }, this.options, options diff --git a/lib/token-types/bearer-token-type.js b/lib/token-types/bearer-token-type.js index 91d338ce..6ffb474d 100644 --- a/lib/token-types/bearer-token-type.js +++ b/lib/token-types/bearer-token-type.js @@ -41,7 +41,7 @@ class BearerTokenType { valueOf() { const object = { access_token: this.accessToken, - token_type: 'Bearer', + token_type: 'Bearer' }; if (this.accessTokenLifetime) { diff --git a/lib/utils/date-util.js b/lib/utils/date-util.js index 4fe32f97..6675a042 100644 --- a/lib/utils/date-util.js +++ b/lib/utils/date-util.js @@ -16,5 +16,5 @@ function getLifetimeFromExpiresAt(expiresAt) { } module.exports = { - getLifetimeFromExpiresAt, + getLifetimeFromExpiresAt }; diff --git a/lib/utils/token-util.js b/lib/utils/token-util.js index 06eec35d..5c9e2392 100644 --- a/lib/utils/token-util.js +++ b/lib/utils/token-util.js @@ -25,5 +25,5 @@ function generateRandomToken() { } module.exports = { - generateRandomToken, + generateRandomToken }; diff --git a/test/compliance/client-authentication_test.js b/test/compliance/client-authentication_test.js index a45363b4..6fe39dbc 100644 --- a/test/compliance/client-authentication_test.js +++ b/test/compliance/client-authentication_test.js @@ -29,7 +29,7 @@ require('chai').should(); const db = new DB(); const auth = new OAuth2Server({ - model: createModel(db), + model: createModel(db) }); const user = db.saveUser({ id: 1, username: 'test', password: 'test' }); @@ -42,13 +42,13 @@ function createDefaultRequest() { grant_type: 'password', username: user.username, password: user.password, - scope, + scope }, headers: { authorization: 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded' }, - method: 'POST', + method: 'POST' }); } diff --git a/test/compliance/client-credential-workflow_test.js b/test/compliance/client-credential-workflow_test.js index df5865f8..4d1b747e 100644 --- a/test/compliance/client-credential-workflow_test.js +++ b/test/compliance/client-credential-workflow_test.js @@ -37,14 +37,14 @@ const oAuth2Server = new OAuth2Server({ // but we need to return a truthy response to const client = db.findClient(_client.id, _client.secret); return client && { ...userDoc }; - }, - }, + } + } }); const clientDoc = db.saveClient({ id: 'client-credential-test-client', secret: 'client-credential-test-secret', - grants: ['client_credentials'], + grants: ['client_credentials'] }); const enabledScope = 'read write'; @@ -74,13 +74,13 @@ describe('ClientCredentials Workflow Compliance (4.4)', function () { const request = createRequest({ body: { grant_type: 'client_credentials', - scope: enabledScope, + scope: enabledScope }, headers: { authorization: 'Basic ' + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded' }, - method: 'POST', + method: 'POST' }); const token = await oAuth2Server.token(request, response); @@ -88,7 +88,7 @@ describe('ClientCredentials Workflow Compliance (4.4)', function () { response.status.should.equal(200); response.headers.should.deep.equal({ 'cache-control': 'no-store', - pragma: 'no-cache', + pragma: 'no-cache' }); response.body.token_type.should.equal('Bearer'); response.body.access_token.should.equal(token.accessToken); @@ -124,9 +124,9 @@ describe('ClientCredentials Workflow Compliance (4.4)', function () { const request = createRequest({ query: {}, headers: { - authorization: `Bearer ${accessToken}`, + authorization: `Bearer ${accessToken}` }, - method: 'GET', + method: 'GET' }); const token = await oAuth2Server.authenticate(request, response); diff --git a/test/compliance/password-grant-type_test.js b/test/compliance/password-grant-type_test.js index 3edf9bbf..bdde8be5 100644 --- a/test/compliance/password-grant-type_test.js +++ b/test/compliance/password-grant-type_test.js @@ -67,7 +67,7 @@ require('chai').should(); const db = new DB(); const auth = new OAuth2Server({ - model: createModel(db), + model: createModel(db) }); const user = db.saveUser({ id: 1, username: 'test', password: 'test' }); @@ -80,13 +80,13 @@ function createDefaultRequest() { grant_type: 'password', username: user.username, password: user.password, - scope, + scope }, headers: { authorization: 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded' }, - method: 'POST', + method: 'POST' }); } @@ -122,10 +122,10 @@ describe('PasswordGrantType Compliance', function () { const authenticationRequest = createRequest({ body: {}, headers: { - Authorization: `Bearer ${token.accessToken}`, + Authorization: `Bearer ${token.accessToken}` }, method: 'GET', - query: {}, + query: {} }); const authenticationResponse = new Response({}); @@ -202,8 +202,8 @@ describe('PasswordGrantType Compliance', function () { const token = await auth.token(request, response, { requireClientAuthentication: { - password: false, - }, + password: false + } }); token.accessToken.should.be.a('string'); @@ -218,8 +218,8 @@ describe('PasswordGrantType Compliance', function () { await auth .token(request, response, { requireClientAuthentication: { - password: false, - }, + password: false + } }) .catch((err) => { err.name.should.equal('invalid_client'); diff --git a/test/compliance/pkce_test.js b/test/compliance/pkce_test.js index d6901749..4569ce6c 100644 --- a/test/compliance/pkce_test.js +++ b/test/compliance/pkce_test.js @@ -66,13 +66,13 @@ describe('PKCE Compliance (RFC 7636)', function () { const userDoc = { id: 'pkce-user-1', username: 'pkceuser', - password: 'pkcepass', + password: 'pkcepass' }; const clientDoc = { id: 'pkce-client', secret: 'pkce-secret', grants: ['authorization_code'], - redirectUris: ['https://client.example/callback'], + redirectUris: ['https://client.example/callback'] }; /** @@ -94,7 +94,7 @@ describe('PKCE Compliance (RFC 7636)', function () { user: userDoc, scope: ['read'], codeChallenge, - codeChallengeMethod: method, + codeChallengeMethod: method }; // store in DB so getAuthorizationCode can find it db.authorizationCodes.set(codeValue, codeDoc); @@ -113,13 +113,13 @@ describe('PKCE Compliance (RFC 7636)', function () { grant_type: 'authorization_code', code, redirect_uri: 'https://client.example/callback', - code_verifier: codeVerifier, + code_verifier: codeVerifier }, headers: { authorization: 'Basic ' + Buffer.from(clientDoc.id + ':' + clientDoc.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded' }, - method: 'POST', + method: 'POST' }); } @@ -155,8 +155,8 @@ describe('PKCE Compliance (RFC 7636)', function () { validateScope: async function (user, client, scope) { return scope; - }, - }, + } + } }); }); @@ -475,7 +475,7 @@ describe('PKCE Compliance (RFC 7636)', function () { user: userDoc, scope: ['read'], codeChallenge: verifier, // plain: challenge === verifier - codeChallengeMethod: 'plain', // RFC 7636 §4.3 default + codeChallengeMethod: 'plain' // RFC 7636 §4.3 default }; db.authorizationCodes.set(codeValue, codeDoc); @@ -534,8 +534,8 @@ describe('PKCE Compliance (RFC 7636)', function () { }, validateScope: async function (user, client, scope) { return scope; - }, - }, + } + } }); const verifier = 'a'.repeat(43); // valid ABNF-length verifier @@ -548,7 +548,7 @@ describe('PKCE Compliance (RFC 7636)', function () { user: userDoc, scope: ['read'], codeChallenge: verifier, // plain: challenge === verifier - codeChallengeMethod: 'plain', + codeChallengeMethod: 'plain' }); const request = tokenRequest(codeValue, verifier); @@ -599,7 +599,7 @@ describe('PKCE Compliance (RFC 7636)', function () { user: userDoc, scope: ['read'], codeChallenge: verifier, - codeChallengeMethod: 'plain', + codeChallengeMethod: 'plain' }); // The attacker uses the stolen code_challenge directly as code_verifier @@ -647,7 +647,7 @@ describe('PKCE Compliance (RFC 7636)', function () { user: userDoc, scope: ['read'], codeChallenge: verifier, - codeChallengeMethod: 'forged-xyz', // invalid method stored in DB that could cause a "plain" fallback if not handled properly + codeChallengeMethod: 'forged-xyz' // invalid method stored in DB that could cause a "plain" fallback if not handled properly }); // The attacker uses the stolen code_challenge directly as code_verifier diff --git a/test/compliance/refresh-token-grant-type_test.js b/test/compliance/refresh-token-grant-type_test.js index 0e8c1627..456f0a54 100644 --- a/test/compliance/refresh-token-grant-type_test.js +++ b/test/compliance/refresh-token-grant-type_test.js @@ -68,14 +68,14 @@ require('chai').should(); const db = new DB(); const auth = new OAuth2Server({ - model: createModel(db), + model: createModel(db) }); const user = db.saveUser({ id: 1, username: 'test', password: 'test' }); const client = db.saveClient({ id: 'a', secret: 'b', - grants: ['password', 'refresh_token'], + grants: ['password', 'refresh_token'] }); const scope = 'read write'; @@ -85,13 +85,13 @@ function createLoginRequest() { grant_type: 'password', username: user.username, password: user.password, - scope, + scope }, headers: { authorization: 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded', + 'content-type': 'application/x-www-form-urlencoded' }, - method: 'POST', + method: 'POST' }); } @@ -101,12 +101,12 @@ function createRefreshRequest(refresh_token) { body: { grant_type: 'refresh_token', refresh_token, - scope, + scope }, headers: { authorization: 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'), - 'content-type': 'application/x-www-form-urlencoded', - }, + 'content-type': 'application/x-www-form-urlencoded' + } }); } diff --git a/test/helpers/model.js b/test/helpers/model.js index ccab6461..dcf0addc 100644 --- a/test/helpers/model.js +++ b/test/helpers/model.js @@ -19,7 +19,7 @@ function createModel(db) { userId: user.id, scope: token.scope, accessTokenExpiresAt: token.accessTokenExpiresAt, - refreshTokenExpiresAt: token.refreshTokenExpiresAt, + refreshTokenExpiresAt: token.refreshTokenExpiresAt }; token.client = client; @@ -50,7 +50,7 @@ function createModel(db) { accessTokenExpiresAt: meta.accessTokenExpiresAt, user: db.findUserById(meta.userId), client: db.findClientById(meta.clientId), - scope: meta.scope, + scope: meta.scope }; } @@ -68,7 +68,7 @@ function createModel(db) { refreshTokenExpiresAt: meta.refreshTokenExpiresAt, user: db.findUserById(meta.userId), client: db.findClientById(meta.clientId), - scope: meta.scope, + scope: meta.scope }; } @@ -92,7 +92,7 @@ function createModel(db) { getAccessToken, getRefreshToken, revokeToken, - verifyScope, + verifyScope }); } diff --git a/test/helpers/request.js b/test/helpers/request.js index 91b278bc..be556a8d 100644 --- a/test/helpers/request.js +++ b/test/helpers/request.js @@ -6,7 +6,7 @@ module.exports = (request) => { body: {}, headers: {}, method: 'GET', - ...request, + ...request }); req.is = function (header) { diff --git a/test/integration/grant-types/abstract-grant-type_test.js b/test/integration/grant-types/abstract-grant-type_test.js index c452b5b3..df0f588a 100644 --- a/test/integration/grant-types/abstract-grant-type_test.js +++ b/test/integration/grant-types/abstract-grant-type_test.js @@ -42,7 +42,7 @@ describe('AbstractGrantType integration', function () { it('should set the `accessTokenLifetime`', function () { const grantType = new AbstractGrantType({ accessTokenLifetime: 123, - model: {}, + model: {} }); grantType.accessTokenLifetime.should.equal(123); @@ -52,7 +52,7 @@ describe('AbstractGrantType integration', function () { const model = Model.from({ async generateAccessToken() {} }); const grantType = new AbstractGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); grantType.model.should.equal(model); @@ -62,7 +62,7 @@ describe('AbstractGrantType integration', function () { const grantType = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); grantType.refreshTokenLifetime.should.equal(456); @@ -74,7 +74,7 @@ describe('AbstractGrantType integration', function () { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const accessToken = await handler.generateAccessToken(); accessToken.should.be.a.sha256(); @@ -84,12 +84,12 @@ describe('AbstractGrantType integration', function () { const model = Model.from({ generateAccessToken: async function () { return 'long-hash-foo-bar'; - }, + } }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const accessToken = await handler.generateAccessToken(); accessToken.should.equal('long-hash-foo-bar'); @@ -99,12 +99,12 @@ describe('AbstractGrantType integration', function () { const model = Model.from({ generateAccessToken: function () { return 'long-hash-foo-bar'; - }, + } }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const accessToken = await handler.generateAccessToken(); accessToken.should.equal('long-hash-foo-bar'); @@ -116,7 +116,7 @@ describe('AbstractGrantType integration', function () { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const refreshToken = await handler.generateRefreshToken(); refreshToken.should.be.a.sha256(); @@ -126,12 +126,12 @@ describe('AbstractGrantType integration', function () { const model = Model.from({ generateRefreshToken: async function () { return 'long-hash-foo-bar'; - }, + } }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const refreshToken = await handler.generateRefreshToken(); refreshToken.should.equal('long-hash-foo-bar'); @@ -141,12 +141,12 @@ describe('AbstractGrantType integration', function () { const model = Model.from({ generateRefreshToken: function () { return 'long-hash-foo-bar'; - }, + } }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: model, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const refreshToken = await handler.generateRefreshToken(); refreshToken.should.equal('long-hash-foo-bar'); @@ -158,7 +158,7 @@ describe('AbstractGrantType integration', function () { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); handler.getAccessTokenExpiresAt().should.be.an.instanceOf(Date); @@ -170,7 +170,7 @@ describe('AbstractGrantType integration', function () { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); handler.getRefreshTokenExpiresAt().should.be.an.instanceOf(Date); @@ -182,13 +182,13 @@ describe('AbstractGrantType integration', function () { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const request = new Request({ body: { scope: 'øå€£‰' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -205,13 +205,13 @@ describe('AbstractGrantType integration', function () { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); should.not.exist(handler.getScope(request)); @@ -221,13 +221,13 @@ describe('AbstractGrantType integration', function () { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); handler.getScope(request).should.eql(['foo']); @@ -242,7 +242,7 @@ describe('AbstractGrantType integration', function () { const handler = new AbstractGrantType({ accessTokenLifetime: 123, model: {}, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const validated = await handler.validateScope(user, client, scope); validated.should.eql(scope); @@ -261,12 +261,12 @@ describe('AbstractGrantType integration', function () { _scope.should.eql(scope); return scope; - }, + } }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); const validated = await handler.validateScope(user, client, scope); validated.should.eql(scope); @@ -287,12 +287,12 @@ describe('AbstractGrantType integration', function () { _scope.should.eql(scope); return type; - }, + } }); const handler = new AbstractGrantType({ accessTokenLifetime: 123, model, - refreshTokenLifetime: 456, + refreshTokenLifetime: 456 }); try { diff --git a/test/integration/grant-types/authorization-code-grant-type_test.js b/test/integration/grant-types/authorization-code-grant-type_test.js index 00c94291..d91b6265 100644 --- a/test/integration/grant-types/authorization-code-grant-type_test.js +++ b/test/integration/grant-types/authorization-code-grant-type_test.js @@ -44,7 +44,7 @@ describe('AuthorizationCodeGrantType integration', function () { it('should throw an error if the model does not implement `revokeAuthorizationCode()`', function () { try { const model = Model.from({ - getAuthorizationCode: function () {}, + getAuthorizationCode: function () {} }); new AuthorizationCodeGrantType({ model: model }); @@ -60,7 +60,7 @@ describe('AuthorizationCodeGrantType integration', function () { try { const model = Model.from({ getAuthorizationCode: function () {}, - revokeAuthorizationCode: function () {}, + revokeAuthorizationCode: function () {} }); new AuthorizationCodeGrantType({ model: model }); @@ -78,11 +78,11 @@ describe('AuthorizationCodeGrantType integration', function () { const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); try { @@ -101,21 +101,21 @@ describe('AuthorizationCodeGrantType integration', function () { return { authorizationCode: 12345, expiresAt: new Date(new Date() * 2), - user: {}, + user: {} }; }, revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 123456789 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -131,17 +131,17 @@ describe('AuthorizationCodeGrantType integration', function () { const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -161,7 +161,7 @@ describe('AuthorizationCodeGrantType integration', function () { expiresAt: new Date(new Date() * 2), client, user, - scope, + scope }; const model = Model.from({ getAuthorizationCode: async function (code) { @@ -200,18 +200,18 @@ describe('AuthorizationCodeGrantType integration', function () { _token.accessTokenExpiresAt.should.be.instanceOf(Date); _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return _token; - }, + } }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 'code-1234' }, headers: {}, method: {}, - query: {}, + query: {} }); const token = await grantType.handle(request, client); @@ -230,23 +230,23 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), - user: {}, + user: {} }; }, revokeAuthorizationCode: function () { return true; }, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.handle(request, client).should.be.an.instanceOf(Promise); @@ -260,23 +260,23 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), - user: {}, + user: {} }; }, revokeAuthorizationCode: function () { return true; }, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.handle(request, client).should.be.an.instanceOf(Promise); @@ -289,17 +289,17 @@ describe('AuthorizationCodeGrantType integration', function () { const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -315,17 +315,17 @@ describe('AuthorizationCodeGrantType integration', function () { const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 'øå€£‰' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -342,17 +342,17 @@ describe('AuthorizationCodeGrantType integration', function () { const model = Model.from({ getAuthorizationCode: async function () {}, revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -371,17 +371,17 @@ describe('AuthorizationCodeGrantType integration', function () { return { authorizationCode: 12345 }; }, revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -400,17 +400,17 @@ describe('AuthorizationCodeGrantType integration', function () { return { authorizationCode: 12345, client: {}, user: {} }; }, revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -429,21 +429,21 @@ describe('AuthorizationCodeGrantType integration', function () { return { authorizationCode: 12345, client: {}, - expiresAt: new Date(), + expiresAt: new Date() }; }, revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -463,21 +463,21 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, expiresAt: new Date(), client: { id: 456 }, - user: {}, + user: {} }; }, revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -498,21 +498,21 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, client: { id: 123 }, expiresAt: date, - user: {}, + user: {} }; }, revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -530,7 +530,7 @@ describe('AuthorizationCodeGrantType integration', function () { client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), redirectUri: 'foobar', - user: {}, + user: {} }; const client = { id: 'foobar' }; const model = Model.from({ @@ -538,17 +538,17 @@ describe('AuthorizationCodeGrantType integration', function () { return authorizationCode; }, revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -565,7 +565,7 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 1234567, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), - user: {}, + user: {} }; const client = { id: 'foobar' }; const model = Model.from({ @@ -574,17 +574,17 @@ describe('AuthorizationCodeGrantType integration', function () { return authorizationCode; }, revokeAuthorizationCode: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); const code = await grantType.getAuthorizationCode(request, client); @@ -596,7 +596,7 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), - user: {}, + user: {} }; const client = { id: 'foobar' }; const model = Model.from({ @@ -604,17 +604,17 @@ describe('AuthorizationCodeGrantType integration', function () { return authorizationCode; }, revokeAuthorizationCode: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); @@ -625,7 +625,7 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), - user: {}, + user: {} }; const client = { id: 'foobar' }; const model = Model.from({ @@ -633,17 +633,17 @@ describe('AuthorizationCodeGrantType integration', function () { return authorizationCode; }, revokeAuthorizationCode: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.getAuthorizationCode(request, client).should.be.an.instanceOf(Promise); @@ -657,24 +657,24 @@ describe('AuthorizationCodeGrantType integration', function () { client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', - user: {}, + user: {} }; const model = Model.from({ getAuthorizationCode: function () {}, revokeAuthorizationCode: function () { return authorizationCode; }, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -693,24 +693,24 @@ describe('AuthorizationCodeGrantType integration', function () { client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', - user: {}, + user: {} }; const model = Model.from({ getAuthorizationCode: function () {}, revokeAuthorizationCode: function () { return true; }, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345, redirect_uri: 'http://bar.foo' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -728,24 +728,24 @@ describe('AuthorizationCodeGrantType integration', function () { client: {}, expiresAt: new Date(new Date() / 2), redirectUri: 'http://foo.bar', - user: {}, + user: {} }; const model = Model.from({ getAuthorizationCode: function () {}, revokeAuthorizationCode: function () { return true; }, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345, redirect_uri: 'http://foo.bar' }, headers: {}, method: {}, - query: {}, + query: {} }); const value = grantType.validateRedirectUri(request, authorizationCode); const isUndefined = value === undefined; @@ -759,7 +759,7 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; const model = Model.from({ getAuthorizationCode: () => should.fail(), @@ -767,11 +767,11 @@ describe('AuthorizationCodeGrantType integration', function () { _code.should.equal(authorizationCode); return true; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const data = await grantType.revokeAuthorizationCode(authorizationCode); @@ -783,7 +783,7 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; const returnTypes = [false, null, undefined, 0, '']; @@ -794,11 +794,11 @@ describe('AuthorizationCodeGrantType integration', function () { _code.should.equal(authorizationCode); return type; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); try { @@ -816,18 +816,18 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: async function () { return true; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); @@ -837,18 +837,18 @@ describe('AuthorizationCodeGrantType integration', function () { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; const model = Model.from({ getAuthorizationCode: () => should.fail(), revokeAuthorizationCode: function () { return authorizationCode; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); grantType.revokeAuthorizationCode(authorizationCode).should.be.an.instanceOf(Promise); }); @@ -876,11 +876,11 @@ describe('AuthorizationCodeGrantType integration', function () { _client.should.equal('fallback'); _scope.should.eql(['fallback']); return ['foo']; - }, + } }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const data = await grantType.saveToken(); data.should.equal(token); @@ -893,11 +893,11 @@ describe('AuthorizationCodeGrantType integration', function () { revokeAuthorizationCode: function () {}, saveToken: async function () { return token; - }, + } }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); @@ -910,11 +910,11 @@ describe('AuthorizationCodeGrantType integration', function () { revokeAuthorizationCode: function () {}, saveToken: function () { return token; - }, + } }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); diff --git a/test/integration/grant-types/client-credentials-grant-type_test.js b/test/integration/grant-types/client-credentials-grant-type_test.js index af9bf945..0f17b78e 100644 --- a/test/integration/grant-types/client-credentials-grant-type_test.js +++ b/test/integration/grant-types/client-credentials-grant-type_test.js @@ -42,7 +42,7 @@ describe('ClientCredentialsGrantType integration', function () { it('should throw an error if the model does not implement `saveToken()`', function () { try { const model = Model.from({ - getUserFromClient: function () {}, + getUserFromClient: function () {} }); new ClientCredentialsGrantType({ model: model }); @@ -59,11 +59,11 @@ describe('ClientCredentialsGrantType integration', function () { it('should throw an error if `request` is missing', async function () { const model = Model.from({ getUserFromClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); try { @@ -79,17 +79,17 @@ describe('ClientCredentialsGrantType integration', function () { it('should throw an error if `client` is missing', async function () { const model = Model.from({ getUserFromClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -132,17 +132,17 @@ describe('ClientCredentialsGrantType integration', function () { _client.should.deep.equal(client); _scope.should.eql(scope); return 'long-access-token-hash'; - }, + } }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: { scope: scope.join(' ') }, headers: {}, method: {}, - query: {}, + query: {} }); const data = await grantType.handle(request, client); @@ -157,17 +157,17 @@ describe('ClientCredentialsGrantType integration', function () { }, saveToken: async function () { return token; - }, + } }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); grantType.handle(request, {}).should.be.an.instanceOf(Promise); @@ -181,17 +181,17 @@ describe('ClientCredentialsGrantType integration', function () { }, saveToken: function () { return token; - }, + } }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); grantType.handle(request, {}).should.be.an.instanceOf(Promise); @@ -202,17 +202,17 @@ describe('ClientCredentialsGrantType integration', function () { it('should throw an error if `user` is missing', function () { const model = Model.from({ getUserFromClient: function () {}, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); return grantType @@ -230,17 +230,17 @@ describe('ClientCredentialsGrantType integration', function () { getUserFromClient: function () { return user; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); return grantType @@ -257,17 +257,17 @@ describe('ClientCredentialsGrantType integration', function () { getUserFromClient: async function () { return user; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); @@ -279,17 +279,17 @@ describe('ClientCredentialsGrantType integration', function () { getUserFromClient: function () { return user; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); grantType.getUserFromClient(request, {}).should.be.an.instanceOf(Promise); @@ -306,11 +306,11 @@ describe('ClientCredentialsGrantType integration', function () { }, validateScope: function () { return ['foo']; - }, + } }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const data = await grantType.saveToken(token); data.should.equal(token); @@ -322,11 +322,11 @@ describe('ClientCredentialsGrantType integration', function () { getUserFromClient: () => should.fail(), saveToken: async function () { return token; - }, + } }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); @@ -338,11 +338,11 @@ describe('ClientCredentialsGrantType integration', function () { getUserFromClient: () => should.fail(), saveToken: function () { return token; - }, + } }); const grantType = new ClientCredentialsGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); diff --git a/test/integration/grant-types/password-grant-type_test.js b/test/integration/grant-types/password-grant-type_test.js index f6ba8329..024156b3 100644 --- a/test/integration/grant-types/password-grant-type_test.js +++ b/test/integration/grant-types/password-grant-type_test.js @@ -43,7 +43,7 @@ describe('PasswordGrantType integration', function () { it('should throw an error if the model does not implement `saveToken()`', function () { try { const model = Model.from({ - getUser: function () {}, + getUser: function () {} }); new PasswordGrantType({ model }); @@ -60,11 +60,11 @@ describe('PasswordGrantType integration', function () { it('should throw an error if `request` is missing', async function () { const model = Model.from({ getUser: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); try { @@ -80,11 +80,11 @@ describe('PasswordGrantType integration', function () { it('should throw an error if `client` is missing', async function () { const model = Model.from({ getUser: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); try { @@ -104,7 +104,7 @@ describe('PasswordGrantType integration', function () { const user = { id: 123456, username: 'foo', - email: 'foo@example.com', + email: 'foo@example.com' }; const model = Model.from({ @@ -139,18 +139,18 @@ describe('PasswordGrantType integration', function () { _token.accessTokenExpiresAt.should.be.instanceOf(Date); _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return token; - }, + } }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: 'foo', password: 'bar', scope: 'baz' }, headers: {}, method: {}, - query: {}, + query: {} }); const data = await grantType.handle(request, client); @@ -166,17 +166,17 @@ describe('PasswordGrantType integration', function () { }, saveToken: async function () { return token; - }, + } }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); const result = await grantType.handle(request, client); @@ -192,17 +192,17 @@ describe('PasswordGrantType integration', function () { }, saveToken: function () { return token; - }, + } }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); const result = await grantType.handle(request, client); @@ -214,18 +214,18 @@ describe('PasswordGrantType integration', function () { it('should throw an error if the request body does not contain `username`', async function () { const model = Model.from({ getUser: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -241,18 +241,18 @@ describe('PasswordGrantType integration', function () { it('should throw an error if the request body does not contain `password`', async function () { const model = Model.from({ getUser: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -268,18 +268,18 @@ describe('PasswordGrantType integration', function () { it('should throw an error if `username` is invalid', async function () { const model = Model.from({ getUser: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: '\r\n', password: 'foobar' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -295,18 +295,18 @@ describe('PasswordGrantType integration', function () { it('should throw an error if `password` is invalid', async function () { const model = Model.from({ getUser: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: 'foobar', password: '\r\n' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -322,18 +322,18 @@ describe('PasswordGrantType integration', function () { it('should throw an error if `user` is missing', async function () { const model = Model.from({ getUser: async () => undefined, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const client = { id: 'foobar' }; const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -354,17 +354,17 @@ describe('PasswordGrantType integration', function () { password.should.equal('bar'); return user; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); const data = await grantType.getUser(request, client); @@ -378,17 +378,17 @@ describe('PasswordGrantType integration', function () { getUser: async function () { return user; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.getUser(request, client).should.be.an.instanceOf(Promise); @@ -401,17 +401,17 @@ describe('PasswordGrantType integration', function () { getUser: function () { return user; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.getUser(request, client).should.be.an.instanceOf(Promise); @@ -436,11 +436,11 @@ describe('PasswordGrantType integration', function () { validateScope: async function (_scope = ['fallback']) { _scope.should.eql(['fallback']); return ['foo']; - }, + } }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); const data = await grantType.saveToken(); @@ -453,11 +453,11 @@ describe('PasswordGrantType integration', function () { getUser: () => should.fail(), saveToken: async function () { return token; - }, + } }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); @@ -469,11 +469,11 @@ describe('PasswordGrantType integration', function () { getUser: () => should.fail(), saveToken: function () { return token; - }, + } }); const grantType = new PasswordGrantType({ accessTokenLifetime: 123, - model, + model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); diff --git a/test/integration/grant-types/refresh-token-grant-type_test.js b/test/integration/grant-types/refresh-token-grant-type_test.js index d44de133..af3e6b63 100644 --- a/test/integration/grant-types/refresh-token-grant-type_test.js +++ b/test/integration/grant-types/refresh-token-grant-type_test.js @@ -45,7 +45,7 @@ describe('RefreshTokenGrantType integration', function () { it('should throw an error if the model does not implement `revokeToken()`', function () { try { const model = Model.from({ - getRefreshToken: () => should.fail(), + getRefreshToken: () => should.fail() }); new RefreshTokenGrantType({ model }); @@ -61,7 +61,7 @@ describe('RefreshTokenGrantType integration', function () { try { const model = Model.from({ getRefreshToken: () => should.fail(), - revokeToken: () => should.fail(), + revokeToken: () => should.fail() }); new RefreshTokenGrantType({ model }); @@ -79,11 +79,11 @@ describe('RefreshTokenGrantType integration', function () { const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); try { @@ -100,17 +100,17 @@ describe('RefreshTokenGrantType integration', function () { const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -130,7 +130,7 @@ describe('RefreshTokenGrantType integration', function () { client: { id: 123 }, user: { name: 'foo' }, scope: ['read', 'write'], - refreshTokenExpiresAt: new Date(new Date() * 2), + refreshTokenExpiresAt: new Date(new Date() * 2) }; const model = Model.from({ getRefreshToken: async function (_refreshToken) { @@ -161,18 +161,18 @@ describe('RefreshTokenGrantType integration', function () { _token.accessTokenExpiresAt.should.be.instanceOf(Date); _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return token; - }, + } }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, - query: {}, + query: {} }); const data = await grantType.handle(request, client); data.should.equal(token); @@ -189,22 +189,22 @@ describe('RefreshTokenGrantType integration', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; }, saveToken: async function () { return { accessToken: 'foo', client: {}, user: {} }; - }, + } }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.handle(request, client).should.be.an.instanceOf(Promise); @@ -221,22 +221,22 @@ describe('RefreshTokenGrantType integration', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; }, saveToken: async function () { return { accessToken: 'foo', client: {}, user: {} }; - }, + } }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.handle(request, client).should.be.an.instanceOf(Promise); @@ -248,24 +248,24 @@ describe('RefreshTokenGrantType integration', function () { accessToken: 'foo', client: { id: 123 }, user: { name: 'foo' }, - refreshTokenExpiresAt: new Date(new Date() * 2), + refreshTokenExpiresAt: new Date(new Date() * 2) }; const model = { getRefreshToken: async function () { return token; }, revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }; const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { refresh_token: 'foobar', scope: 'read' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -285,17 +285,17 @@ describe('RefreshTokenGrantType integration', function () { const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -313,17 +313,17 @@ describe('RefreshTokenGrantType integration', function () { const model = Model.from({ getRefreshToken: async function () {}, revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: { refresh_token: '12345' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -343,17 +343,17 @@ describe('RefreshTokenGrantType integration', function () { return {}; }, revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -373,17 +373,17 @@ describe('RefreshTokenGrantType integration', function () { return { accessToken: 'foo', client: {} }; }, revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -403,17 +403,17 @@ describe('RefreshTokenGrantType integration', function () { return { accessToken: 'foo', client: { id: 456 }, user: {} }; }, revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -433,17 +433,17 @@ describe('RefreshTokenGrantType integration', function () { return { client: { id: 456 }, user: {} }; }, revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: { refresh_token: 'øå€£‰' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -463,17 +463,17 @@ describe('RefreshTokenGrantType integration', function () { return { accessToken: 'foo', client: { id: 456 }, user: {} }; }, revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -495,21 +495,21 @@ describe('RefreshTokenGrantType integration', function () { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: date, - user: {}, + user: {} }; }, revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -530,21 +530,21 @@ describe('RefreshTokenGrantType integration', function () { accessToken: 'foo', client: { id: 123 }, refreshTokenExpiresAt: 'stringvalue', - user: {}, + user: {} }; }, revokeToken: () => should.fail(), - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); const request = new Request({ body: { refresh_token: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -562,7 +562,7 @@ describe('RefreshTokenGrantType integration', function () { const token = { accessToken: 'foo', client: { id: 123 }, - user: { name: 'foobar' }, + user: { name: 'foobar' } }; const model = Model.from({ getRefreshToken: async function (_refreshToken) { @@ -581,17 +581,17 @@ describe('RefreshTokenGrantType integration', function () { _token.accessTokenExpiresAt.should.be.instanceOf(Date); _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return token; - }, + } }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { refresh_token: 'foobar_refresh' }, headers: {}, method: {}, - query: {}, + query: {} }); const data = await grantType.getRefreshToken(request, client); @@ -606,17 +606,17 @@ describe('RefreshTokenGrantType integration', function () { return token; }, revokeToken: async function () {}, - saveToken: async function () {}, + saveToken: async function () {} }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); @@ -630,17 +630,17 @@ describe('RefreshTokenGrantType integration', function () { return token; }, revokeToken: async function () {}, - saveToken: async function () {}, + saveToken: async function () {} }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); const request = new Request({ body: { refresh_token: 'foobar' }, headers: {}, method: {}, - query: {}, + query: {} }); grantType.getRefreshToken(request, client).should.be.an.instanceOf(Promise); @@ -652,11 +652,11 @@ describe('RefreshTokenGrantType integration', function () { const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: async () => {}, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model, + model }); try { @@ -673,7 +673,7 @@ describe('RefreshTokenGrantType integration', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; const model = Model.from({ getRefreshToken: () => should.fail(), @@ -681,11 +681,11 @@ describe('RefreshTokenGrantType integration', function () { _token.should.deep.equal(token); return token; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); const data = await grantType.revokeToken(token); @@ -697,18 +697,18 @@ describe('RefreshTokenGrantType integration', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: async function () { return token; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); grantType.revokeToken(token).should.be.an.instanceOf(Promise); @@ -719,18 +719,18 @@ describe('RefreshTokenGrantType integration', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; const model = Model.from({ getRefreshToken: () => should.fail(), revokeToken: function () { return token; }, - saveToken: () => should.fail(), + saveToken: () => should.fail() }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); grantType.revokeToken(token).should.be.an.instanceOf(Promise); @@ -754,11 +754,11 @@ describe('RefreshTokenGrantType integration', function () { _token.accessTokenExpiresAt.should.be.instanceOf(Date); _token.refreshTokenExpiresAt.should.be.instanceOf(Date); return { ..._token }; - }, + } }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); const data = await grantType.saveToken(user, client, scope); @@ -776,11 +776,11 @@ describe('RefreshTokenGrantType integration', function () { revokeToken: () => should.fail(), saveToken: async function () { return token; - }, + } }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); @@ -793,11 +793,11 @@ describe('RefreshTokenGrantType integration', function () { revokeToken: () => should.fail(), saveToken: function () { return token; - }, + } }); const grantType = new RefreshTokenGrantType({ accessTokenLifetime: 123, - model, + model }); grantType.saveToken(token).should.be.an.instanceOf(Promise); diff --git a/test/integration/handlers/authenticate-handler_test.js b/test/integration/handlers/authenticate-handler_test.js index f3e5e92d..98cb29aa 100644 --- a/test/integration/handlers/authenticate-handler_test.js +++ b/test/integration/handlers/authenticate-handler_test.js @@ -49,7 +49,7 @@ describe('AuthenticateHandler integration', function () { try { new AuthenticateHandler({ model: { getAccessToken: function () {} }, - scope: ['foobar'], + scope: ['foobar'] }); should.fail(); @@ -64,7 +64,7 @@ describe('AuthenticateHandler integration', function () { new AuthenticateHandler({ addAcceptedScopesHeader: true, model: { getAccessToken: function () {} }, - scope: ['foobar'], + scope: ['foobar'] }); should.fail(); @@ -80,7 +80,7 @@ describe('AuthenticateHandler integration', function () { addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: { getAccessToken: function () {} }, - scope: ['foobar'], + scope: ['foobar'] }); should.fail(); @@ -100,13 +100,13 @@ describe('AuthenticateHandler integration', function () { it('should set the `scope`', function () { const model = Model.from({ getAccessToken: function () {}, - verifyScope: function () {}, + verifyScope: function () {} }); const grantType = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: 'foobar', + scope: 'foobar' }); grantType.scope.should.eql(['foobar']); @@ -119,7 +119,7 @@ describe('AuthenticateHandler integration', function () { const values = [undefined, null, {}, [], new Date(), new Request()]; for (const request of values) { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); try { @@ -140,12 +140,12 @@ describe('AuthenticateHandler integration', function () { body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); for (const response of values) { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); try { await handler.handle(request, response); @@ -162,14 +162,14 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: function () { throw new UnauthorizedRequestError(); - }, + } }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -185,14 +185,14 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: function () { throw new InvalidRequestError(); - }, + } }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -208,14 +208,14 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: function () { throw new InvalidTokenError(); - }, + } }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -231,14 +231,14 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: function () { throw new InsufficientScopeError(); - }, + } }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -254,14 +254,14 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: function () { throw new AccessDeniedError('Cannot request this access token'); - }, + } }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -278,14 +278,14 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: function () { throw new Error('Unhandled exception'); - }, + } }); const handler = new AuthenticateHandler({ model: model }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -301,7 +301,7 @@ describe('AuthenticateHandler integration', function () { it('should return an access token', function () { const accessToken = { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; const model = Model.from({ getAccessToken: function () { @@ -309,19 +309,19 @@ describe('AuthenticateHandler integration', function () { }, verifyScope: function () { return true; - }, + } }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: ['foo'], + scope: ['foo'] }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -336,7 +336,7 @@ describe('AuthenticateHandler integration', function () { it('should return an access token (deprecated)', function () { const accessToken = { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; const model = Model.from({ getAccessToken: function () { @@ -344,19 +344,19 @@ describe('AuthenticateHandler integration', function () { }, verifyScope: function () { return true; - }, + } }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: 'foo', + scope: 'foo' }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -372,13 +372,13 @@ describe('AuthenticateHandler integration', function () { describe('getTokenFromRequest()', function () { it('should throw an error if more than one authentication method is used', async function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: { access_token: 'foo' }, + query: { access_token: 'foo' } }); try { @@ -393,13 +393,13 @@ describe('AuthenticateHandler integration', function () { it('should throw an error if `accessToken` is missing', async function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -416,15 +416,15 @@ describe('AuthenticateHandler integration', function () { describe('getTokenFromRequestHeader()', function () { it('should throw an error if the token is malformed', async function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: {}, headers: { - Authorization: 'foobar', + Authorization: 'foobar' }, method: {}, - query: {}, + query: {} }); try { @@ -439,15 +439,15 @@ describe('AuthenticateHandler integration', function () { it('should return the bearer token', function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: {}, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const bearerToken = handler.getTokenFromRequestHeader(request); @@ -459,7 +459,7 @@ describe('AuthenticateHandler integration', function () { describe('getTokenFromRequestQuery()', function () { it('should throw an error if the query contains a token', async function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); try { @@ -475,7 +475,7 @@ describe('AuthenticateHandler integration', function () { it('should return the bearer token if `allowBearerTokensInQueryString` is true', function () { const handler = new AuthenticateHandler({ allowBearerTokensInQueryString: true, - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); handler.getTokenFromRequestQuery({ query: { access_token: 'foo' } }).should.equal('foo'); @@ -485,13 +485,13 @@ describe('AuthenticateHandler integration', function () { describe('getTokenFromRequestBody()', function () { it('should throw an error if the method is `GET`', async function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: { access_token: 'foo' }, headers: {}, method: 'GET', - query: {}, + query: {} }); try { @@ -506,13 +506,13 @@ describe('AuthenticateHandler integration', function () { it('should throw an error if the media type is not `application/x-www-form-urlencoded`', async function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: { access_token: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -527,16 +527,16 @@ describe('AuthenticateHandler integration', function () { it('should return the bearer token', function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: { access_token: 'foo' }, headers: { 'content-type': 'application/x-www-form-urlencoded', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: {}, - query: {}, + query: {} }); handler.getTokenFromRequestBody(request).should.equal('foo'); @@ -546,7 +546,7 @@ describe('AuthenticateHandler integration', function () { describe('getAccessToken()', function () { it('should throw an error if `accessToken` is missing', function () { const model = Model.from({ - getAccessToken: function () {}, + getAccessToken: function () {} }); const handler = new AuthenticateHandler({ model: model }); @@ -563,7 +563,7 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: function () { return {}; - }, + } }); const handler = new AuthenticateHandler({ model: model }); @@ -581,7 +581,7 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: function () { return accessToken; - }, + } }); const handler = new AuthenticateHandler({ model: model }); @@ -597,7 +597,7 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: async function () { return { user: {} }; - }, + } }); const handler = new AuthenticateHandler({ model: model }); @@ -608,7 +608,7 @@ describe('AuthenticateHandler integration', function () { const model = Model.from({ getAccessToken: function () { return { user: {} }; - }, + } }); const handler = new AuthenticateHandler({ model: model }); @@ -620,7 +620,7 @@ describe('AuthenticateHandler integration', function () { it('should throw an error if `accessToken` is expired', async function () { const accessToken = { accessTokenExpiresAt: new Date(new Date() / 2) }; const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); try { @@ -636,10 +636,10 @@ describe('AuthenticateHandler integration', function () { it('should return an access token', function () { const accessToken = { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); handler.validateAccessToken(accessToken).should.equal(accessToken); @@ -652,13 +652,13 @@ describe('AuthenticateHandler integration', function () { getAccessToken: function () {}, verifyScope: function () { return false; - }, + } }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: 'foo', + scope: 'foo' }); return handler @@ -675,13 +675,13 @@ describe('AuthenticateHandler integration', function () { getAccessToken: function () {}, verifyScope: function () { return false; - }, + } }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: ['foo'], + scope: ['foo'] }); return handler @@ -698,13 +698,13 @@ describe('AuthenticateHandler integration', function () { getAccessToken: function () {}, verifyScope: function () { return true; - }, + } }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: 'foo', + scope: 'foo' }); handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); @@ -715,13 +715,13 @@ describe('AuthenticateHandler integration', function () { getAccessToken: function () {}, verifyScope: function () { return true; - }, + } }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: ['foo'], + scope: ['foo'] }); handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); @@ -732,13 +732,13 @@ describe('AuthenticateHandler integration', function () { getAccessToken: function () {}, verifyScope: function () { return true; - }, + } }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: 'foo', + scope: 'foo' }); handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); @@ -749,13 +749,13 @@ describe('AuthenticateHandler integration', function () { getAccessToken: function () {}, verifyScope: function () { return true; - }, + } }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: ['foo'], + scope: ['foo'] }); handler.verifyScope(['foo']).should.be.an.instanceOf(Promise); @@ -766,12 +766,12 @@ describe('AuthenticateHandler integration', function () { it('should not set the `X-Accepted-OAuth-Scopes` header if `scope` is not specified', function () { const model = Model.from({ getAccessToken: function () {}, - verifyScope: function () {}, + verifyScope: function () {} }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, - model: model, + model: model }); const response = new Response({ body: {}, headers: {} }); @@ -783,13 +783,13 @@ describe('AuthenticateHandler integration', function () { it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified (deprecated)', function () { const model = Model.from({ getAccessToken: function () {}, - verifyScope: function () {}, + verifyScope: function () {} }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, - scope: 'foo bar', + scope: 'foo bar' }); const response = new Response({ body: {}, headers: {} }); @@ -801,13 +801,13 @@ describe('AuthenticateHandler integration', function () { it('should set the `X-Accepted-OAuth-Scopes` header if `scope` is specified', function () { const model = Model.from({ getAccessToken: function () {}, - verifyScope: function () {}, + verifyScope: function () {} }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: false, model: model, - scope: ['foo', 'bar'], + scope: ['foo', 'bar'] }); const response = new Response({ body: {}, headers: {} }); @@ -819,12 +819,12 @@ describe('AuthenticateHandler integration', function () { it('should not set the `X-Authorized-OAuth-Scopes` header if `scope` is not specified', function () { const model = Model.from({ getAccessToken: function () {}, - verifyScope: function () {}, + verifyScope: function () {} }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, - model: model, + model: model }); const response = new Response({ body: {}, headers: {} }); @@ -836,13 +836,13 @@ describe('AuthenticateHandler integration', function () { it('should set the `X-Authorized-OAuth-Scopes` header (deprecated)', function () { const model = Model.from({ getAccessToken: function () {}, - verifyScope: function () {}, + verifyScope: function () {} }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, - scope: 'foo bar', + scope: 'foo bar' }); const response = new Response({ body: {}, headers: {} }); @@ -854,13 +854,13 @@ describe('AuthenticateHandler integration', function () { it('should set the `X-Authorized-OAuth-Scopes` header', function () { const model = Model.from({ getAccessToken: function () {}, - verifyScope: function () {}, + verifyScope: function () {} }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: false, addAuthorizedScopesHeader: true, model: model, - scope: ['foo', 'bar'], + scope: ['foo', 'bar'] }); const response = new Response({ body: {}, headers: {} }); diff --git a/test/integration/handlers/authorize-handler_test.js b/test/integration/handlers/authorize-handler_test.js index 2f3a9655..91927601 100644 --- a/test/integration/handlers/authorize-handler_test.js +++ b/test/integration/handlers/authorize-handler_test.js @@ -26,7 +26,7 @@ const createModel = (model = {}) => { getAccessToken: () => should.fail(), getClient: () => should.fail(), saveAuthorizationCode: () => should.fail(), - ...model, + ...model }); }; @@ -70,7 +70,7 @@ describe('AuthorizeHandler integration', function () { try { new AuthorizeHandler({ authorizationCodeLifetime: 120, - model: { getClient: () => should.fail() }, + model: { getClient: () => should.fail() } }); should.fail(); } catch (e) { @@ -82,7 +82,7 @@ describe('AuthorizeHandler integration', function () { it('should throw an error if the model does not implement `getAccessToken()`', function () { const model = Model.from({ getClient: () => should.fail(), - saveAuthorizationCode: () => should.fail(), + saveAuthorizationCode: () => should.fail() }); try { @@ -99,7 +99,7 @@ describe('AuthorizeHandler integration', function () { const model = createModel(); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.authorizationCodeLifetime.should.equal(120); @@ -113,7 +113,7 @@ describe('AuthorizeHandler integration', function () { new AuthorizeHandler({ authenticateHandler, authorizationCodeLifetime: 120, - model, + model }); should.fail(); } catch (e) { @@ -126,7 +126,7 @@ describe('AuthorizeHandler integration', function () { const model = createModel(); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.authenticateHandler.should.be.an.instanceOf(AuthenticateHandler); }); @@ -142,7 +142,7 @@ describe('AuthorizeHandler integration', function () { const handler = new AuthorizeHandler({ authenticateHandler, authorizationCodeLifetime: 120, - model, + model }); handler.authenticateHandler.should.be.an.instanceOf(CustomAuthenticateHandler); handler.authenticateHandler.should.not.be.an.instanceOf(AuthenticateHandler); @@ -152,7 +152,7 @@ describe('AuthorizeHandler integration', function () { const model = createModel(); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.model.should.equal(model); }); @@ -163,7 +163,7 @@ describe('AuthorizeHandler integration', function () { const model = createModel(); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); try { @@ -179,13 +179,13 @@ describe('AuthorizeHandler integration', function () { const model = createModel(); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -201,39 +201,39 @@ describe('AuthorizeHandler integration', function () { const client = { id: 'client-12345', grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const model = createModel({ getAccessToken: async function (_token) { _token.should.equal('foobarbazmootoken'); return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function (clientId, clientSecret) { clientId.should.equal(client.id); (clientSecret === null).should.equal(true); return { ...client }; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: client.id, - response_type: 'code', + response_type: 'code' }, method: {}, headers: { - Authorization: 'Bearer foobarbazmootoken', + Authorization: 'Bearer foobarbazmootoken' }, query: { state: 'foobar', - allowed: 'false', - }, + allowed: 'false' + } }); const response = new Response({ body: {}, headers: {} }); @@ -256,36 +256,36 @@ describe('AuthorizeHandler integration', function () { getAccessToken: async function () { return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function () { return { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: async function () { throw new CustomError('Unhandled exception'); - }, + } }); class CustomError extends Error {} const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, query: { - state: 'foobar', - }, + state: 'foobar' + } }); const response = new Response({ body: {}, headers: {} }); @@ -308,35 +308,35 @@ describe('AuthorizeHandler integration', function () { getAccessToken: async function () { return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function () { return { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: async function () { throw new AccessDeniedError('Cannot request this auth code'); - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, query: { - state: 'foobar', - }, + state: 'foobar' + } }); const response = new Response({ body: {}, headers: {} }); @@ -358,7 +358,7 @@ describe('AuthorizeHandler integration', function () { const client = { id: 'client-12343434', grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const model = createModel({ getAccessToken: async function (_token) { @@ -366,7 +366,7 @@ describe('AuthorizeHandler integration', function () { return { client, user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function (clientId, clientSecret) { @@ -377,26 +377,26 @@ describe('AuthorizeHandler integration', function () { saveAuthorizationCode: async function () { return { authorizationCode: 'fooobar-long-authzcode-?', - client, + client }; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: client.id, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: 'Bearer foobarbaztokenmoo', + Authorization: 'Bearer foobarbaztokenmoo' }, method: {}, query: { - state: 'foobarbazstatemoo', - }, + state: 'foobarbazstatemoo' + } }); const response = new Response({ body: {}, headers: {} }); const data = await handler.handle(request, response); @@ -413,36 +413,36 @@ describe('AuthorizeHandler integration', function () { getAccessToken: async function () { return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function () { return { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: async function () { return {}; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, query: { scope: [], - state: 'foobar', - }, + state: 'foobar' + } }); const response = new Response({ body: {}, headers: {} }); @@ -464,14 +464,14 @@ describe('AuthorizeHandler integration', function () { it('should redirect to a successful response if `model.validateScope` is not defined', async function () { const client = { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const model = Model.from({ getAccessToken: function () { return { client: client, user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: function () { @@ -479,31 +479,31 @@ describe('AuthorizeHandler integration', function () { }, saveAuthorizationCode: function () { return { authorizationCode: 'fooobar-long-authzcode-?', client }; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, query: { scope: 'read', - state: 'foobarbazstatemoo', - }, + state: 'foobarbazstatemoo' + } }); const response = new Response({ body: {}, headers: {} }); const data = await handler.handle(request, response); data.should.deep.equal({ authorizationCode: 'fooobar-long-authzcode-?', - client: client, + client: client }); response.status.should.equal(302); response @@ -515,14 +515,14 @@ describe('AuthorizeHandler integration', function () { const client = { id: 12345, grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const model = Model.from({ getAccessToken: async function () { return { client: client, user: { name: 'foouser' }, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function () { @@ -534,25 +534,25 @@ describe('AuthorizeHandler integration', function () { validateScope: async function (_user, _client, _scope) { _scope.should.eql(['read']); return false; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, query: { scope: 'read', - state: 'foobar', - }, + state: 'foobar' + } }); const response = new Response({ body: {}, headers: {} }); @@ -576,33 +576,33 @@ describe('AuthorizeHandler integration', function () { getAccessToken: async function () { return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function () { return { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: async function () { throw new AccessDeniedError('Cannot request this auth code'); - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -626,33 +626,33 @@ describe('AuthorizeHandler integration', function () { getAccessToken: async function () { return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function () { return { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: () => should.fail(), // should fail before call + saveAuthorizationCode: () => should.fail() // should fail before call }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, - response_type: 'test', + response_type: 'test' }, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, query: { - state: 'foobar', - }, + state: 'foobar' + } }); const response = new Response({ body: {}, headers: {} }); @@ -674,14 +674,14 @@ describe('AuthorizeHandler integration', function () { it('should return the `code` if successful', async function () { const client = { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const model = Model.from({ getAccessToken: async function () { return { client: client, user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function () { @@ -690,31 +690,31 @@ describe('AuthorizeHandler integration', function () { generateAuthorizationCode: async () => 'some-code', saveAuthorizationCode: async function (code) { return { authorizationCode: code.authorizationCode, client: client }; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, query: { - state: 'foobar', - }, + state: 'foobar' + } }); const response = new Response({ body: {}, headers: {} }); const data = await handler.handle(request, response); data.should.eql({ authorizationCode: 'some-code', - client: client, + client: client }); }); @@ -725,7 +725,7 @@ describe('AuthorizeHandler integration', function () { const client = { id: 'client-1322132131', grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const authorizationCode = 'long-authz-code'; const accessTokenDoc = { @@ -733,7 +733,7 @@ describe('AuthorizeHandler integration', function () { client, user, scope, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; const model = Model.from({ getClient: async function (clientId, clientSecret) { @@ -768,23 +768,23 @@ describe('AuthorizeHandler integration', function () { _user.should.deep.equal(user); _client.should.deep.equal(client); return { ...code, client, user }; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: client.id, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: `Bearer ${accessTokenDoc.accessToken}`, + Authorization: `Bearer ${accessTokenDoc.accessToken}` }, method: {}, - query: { state, scope: scope.join(' ') }, + query: { state, scope: scope.join(' ') } }); const response = new Response({ body: {}, headers: {} }); @@ -804,18 +804,18 @@ describe('AuthorizeHandler integration', function () { handle: async function () { // all good return { ...user }; - }, + } }; const client = { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const model = Model.from({ getAccessToken: async function () { return { client: client, user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: async function () { @@ -824,32 +824,32 @@ describe('AuthorizeHandler integration', function () { generateAuthorizationCode: async () => 'some-code', saveAuthorizationCode: async function (code) { return { authorizationCode: code.authorizationCode, client: client }; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, model, - authenticateHandler, + authenticateHandler }); const request = new Request({ body: { client_id: 12345, - response_type: 'code', + response_type: 'code' }, headers: { - Authorization: 'Bearer foo', + Authorization: 'Bearer foo' }, method: {}, query: { - state: 'foobar', - }, + state: 'foobar' + } }); const response = new Response({ body: {}, headers: {} }); const data = await handler.handle(request, response); data.should.eql({ authorizationCode: 'some-code', - client: client, + client: client }); }); }); @@ -859,11 +859,11 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); return handler @@ -881,11 +881,11 @@ describe('AuthorizeHandler integration', function () { }, getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); @@ -898,11 +898,11 @@ describe('AuthorizeHandler integration', function () { }, getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.generateAuthorizationCode().should.be.an.instanceOf(Promise); @@ -914,11 +914,11 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.getAuthorizationCodeLifetime().should.be.an.instanceOf(Date); @@ -930,17 +930,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler .validateRedirectUri('http://example.com/a', { - redirectUris: ['http://example.com/a'], + redirectUris: ['http://example.com/a'] }) .should.be.an.instanceOf(Promise); }); @@ -952,12 +952,12 @@ describe('AuthorizeHandler integration', function () { saveAuthorizationCode: function () {}, validateRedirectUri: async function () { return true; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.validateRedirectUri('http://example.com/a', {}).should.be.an.instanceOf(Promise); @@ -970,12 +970,12 @@ describe('AuthorizeHandler integration', function () { saveAuthorizationCode: function () {}, validateRedirectUri: function () { return true; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.validateRedirectUri('http://example.com/a', {}).should.be.an.instanceOf(Promise); @@ -987,17 +987,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1014,17 +1014,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 'øå€£‰', response_type: 'code' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1041,21 +1041,21 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, response_type: 'code', - redirect_uri: 'foobar', + redirect_uri: 'foobar' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1072,17 +1072,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1100,17 +1100,17 @@ describe('AuthorizeHandler integration', function () { getClient: function () { return {}; }, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1128,17 +1128,17 @@ describe('AuthorizeHandler integration', function () { getClient: function () { return { grants: [] }; }, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1156,17 +1156,17 @@ describe('AuthorizeHandler integration', function () { getClient: function () { return { grants: ['authorization_code'] }; }, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, response_type: 'code' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1184,24 +1184,24 @@ describe('AuthorizeHandler integration', function () { getClient: function () { return { grants: ['authorization_code'], - redirectUris: ['https://example.com'], + redirectUris: ['https://example.com'] }; }, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345, response_type: 'code', - redirect_uri: 'https://foobar.com', + redirect_uri: 'https://foobar.com' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1219,20 +1219,20 @@ describe('AuthorizeHandler integration', function () { getClient: async function () { return { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); handler.getClient(request).should.be.an.instanceOf(Promise); @@ -1244,20 +1244,20 @@ describe('AuthorizeHandler integration', function () { getClient: function () { return { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; }, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { client_id: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); handler.getClient(request).should.be.an.instanceOf(Promise); @@ -1267,24 +1267,24 @@ describe('AuthorizeHandler integration', function () { it('should return a client', function () { const client = { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const model = Model.from({ getAccessToken: function () {}, getClient: function () { return client; }, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, - query: { client_id: 12345 }, + query: { client_id: 12345 } }); return handler @@ -1302,17 +1302,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { scope: 'øå€£‰' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1330,17 +1330,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { scope: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); handler.getScope(request).should.eql(['foo']); @@ -1352,17 +1352,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: { scope: 'foo' }, + query: { scope: 'foo' } }); handler.getScope(request).should.eql(['foo']); @@ -1375,18 +1375,18 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ allowEmptyState: false, authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1403,18 +1403,18 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ allowEmptyState: true, authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); const state = handler.getState(request); should.equal(state, undefined); @@ -1424,17 +1424,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: { state: 'øå€£‰' }, + query: { state: 'øå€£‰' } }); try { @@ -1452,17 +1452,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { state: 'foobar' }, headers: {}, method: {}, - query: {}, + query: {} }); handler.getState(request).should.equal('foobar'); @@ -1474,17 +1474,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: { state: 'foobar' }, + query: { state: 'foobar' } }); handler.getState(request).should.equal('foobar'); @@ -1497,18 +1497,18 @@ describe('AuthorizeHandler integration', function () { const authenticateHandler = { handle: function () {} }; const model = Model.from({ getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); const response = new Response(); @@ -1527,21 +1527,21 @@ describe('AuthorizeHandler integration', function () { getAccessToken: function () { return { user: user, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -1562,11 +1562,11 @@ describe('AuthorizeHandler integration', function () { getClient: function () {}, saveAuthorizationCode: function () { return authorizationCode; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); return handler @@ -1583,11 +1583,11 @@ describe('AuthorizeHandler integration', function () { getClient: function () {}, saveAuthorizationCode: async function () { return {}; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); @@ -1599,11 +1599,11 @@ describe('AuthorizeHandler integration', function () { getClient: function () {}, saveAuthorizationCode: function () { return {}; - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); handler.saveAuthorizationCode('foo', 'bar', 'biz', 'baz').should.be.an.instanceOf(Promise); @@ -1615,17 +1615,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1642,17 +1642,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { response_type: 'foobar' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1670,17 +1670,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { response_type: 'code' }, headers: {}, method: {}, - query: {}, + query: {} }); const ResponseType = handler.getResponseType(request); @@ -1693,17 +1693,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: { response_type: 'code' }, + query: { response_type: 'code' } }); const ResponseType = handler.getResponseType(request); @@ -1717,11 +1717,11 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const responseType = new CodeResponseType(12345); const redirectUri = handler.buildSuccessRedirectUri('http://example.com/cb', responseType); @@ -1736,11 +1736,11 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); @@ -1752,11 +1752,11 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const redirectUri = handler.buildErrorRedirectUri('http://example.com/cb', error); @@ -1771,11 +1771,11 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const response = new Response({ body: {}, headers: {} }); const uri = url.parse('http://example.com/cb'); @@ -1794,17 +1794,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { code_challenge_method: method }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1826,17 +1826,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { code_challenge_method: 'S256' }, headers: {}, method: {}, - query: {}, + query: {} }); const codeChallengeMethod = handler.getCodeChallengeMethod(request); @@ -1847,17 +1847,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { code_challenge_method: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1875,17 +1875,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); const codeChallengeMethod = handler.getCodeChallengeMethod(request); @@ -1896,18 +1896,18 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, enablePlainPKCE: true, - model, + model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); const codeChallengeMethod = handler.getCodeChallengeMethod(request); @@ -1920,17 +1920,17 @@ describe('AuthorizeHandler integration', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model, + model }); const request = new Request({ body: { code_challenge: 'challenge' }, headers: {}, method: {}, - query: {}, + query: {} }); const codeChallengeMethod = handler.getCodeChallenge(request); diff --git a/test/integration/handlers/token-handler_test.js b/test/integration/handlers/token-handler_test.js index 010a9292..ba68e8d4 100644 --- a/test/integration/handlers/token-handler_test.js +++ b/test/integration/handlers/token-handler_test.js @@ -67,7 +67,7 @@ describe('TokenHandler integration', function () { new TokenHandler({ accessTokenLifetime: 120, model: {}, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); should.fail(); @@ -81,12 +81,12 @@ describe('TokenHandler integration', function () { const accessTokenLifetime = {}; const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: accessTokenLifetime, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); handler.accessTokenLifetime.should.equal(accessTokenLifetime); @@ -96,13 +96,13 @@ describe('TokenHandler integration', function () { const alwaysIssueNewRefreshToken = true; const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, - alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, + alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); @@ -112,13 +112,13 @@ describe('TokenHandler integration', function () { const alwaysIssueNewRefreshToken = false; const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 123, model: model, refreshTokenLifetime: 120, - alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken, + alwaysIssueNewRefreshToken: alwaysIssueNewRefreshToken }); handler.alwaysIssueNewRefreshToken.should.equal(alwaysIssueNewRefreshToken); @@ -127,12 +127,12 @@ describe('TokenHandler integration', function () { it('should return the default `alwaysIssueNewRefreshToken` value', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 123, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); handler.alwaysIssueNewRefreshToken.should.equal(true); @@ -142,13 +142,13 @@ describe('TokenHandler integration', function () { const extendedGrantTypes = { foo: 'bar' }; const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, extendedGrantTypes: extendedGrantTypes, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); handler.grantTypes.should.deep.include(extendedGrantTypes); }); @@ -156,12 +156,12 @@ describe('TokenHandler integration', function () { it('should set the `model`', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); handler.model.should.equal(model); @@ -171,12 +171,12 @@ describe('TokenHandler integration', function () { const refreshTokenLifetime = {}; const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: refreshTokenLifetime, + refreshTokenLifetime: refreshTokenLifetime }); handler.refreshTokenLifetime.should.equal(refreshTokenLifetime); @@ -187,12 +187,12 @@ describe('TokenHandler integration', function () { it('should throw an error if `request` is missing', async function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); try { @@ -208,18 +208,18 @@ describe('TokenHandler integration', function () { it('should throw an error if `response` is missing', async function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -235,18 +235,18 @@ describe('TokenHandler integration', function () { it('should throw an error if the method is not `POST`', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: {}, method: 'GET', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -262,18 +262,18 @@ describe('TokenHandler integration', function () { it('should throw an error if the media type is not `application/x-www-form-urlencoded`', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: {}, method: 'POST', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -289,21 +289,21 @@ describe('TokenHandler integration', function () { it('should throw the error if an oauth error is thrown', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: { 'content-type': 'application/x-www-form-urlencoded', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: 'POST', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -322,12 +322,12 @@ describe('TokenHandler integration', function () { throw new Error('Unhandled exception'); }, getUser: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -335,14 +335,14 @@ describe('TokenHandler integration', function () { client_secret: 'secret', grant_type: 'password', password: 'bar', - username: 'foo', + username: 'foo' }, headers: { 'content-type': 'application/x-www-form-urlencoded', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: 'POST', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -362,12 +362,12 @@ describe('TokenHandler integration', function () { throw new Error('Unhandled exception'); }, getUser: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -375,14 +375,14 @@ describe('TokenHandler integration', function () { client_secret: 'secret', grant_type: 'password', password: 'bar', - username: 'foo', + username: 'foo' }, headers: { 'content-type': 'application/x-www-form-urlencoded', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: 'POST', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -392,7 +392,7 @@ describe('TokenHandler integration', function () { .catch(function () { response.body.should.eql({ error: 'server_error', - error_description: 'Unhandled exception', + error_description: 'Unhandled exception' }); response.status.should.equal(503); }); @@ -404,7 +404,7 @@ describe('TokenHandler integration', function () { client: {}, refreshToken: 'bar', scope: ['foobar'], - user: {}, + user: {} }; const model = Model.from({ getClient: function () { @@ -418,12 +418,12 @@ describe('TokenHandler integration', function () { }, validateScope: function () { return ['baz']; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -432,14 +432,14 @@ describe('TokenHandler integration', function () { username: 'foo', password: 'bar', grant_type: 'password', - scope: 'baz', + scope: 'baz' }, headers: { 'content-type': 'application/x-www-form-urlencoded', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: 'POST', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -458,7 +458,7 @@ describe('TokenHandler integration', function () { refreshToken: 'bar', scope: ['baz'], user: {}, - foo: 'bar', + foo: 'bar' }; const model = Model.from({ getClient: function () { @@ -472,12 +472,12 @@ describe('TokenHandler integration', function () { }, validateScope: function () { return ['baz']; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -486,14 +486,14 @@ describe('TokenHandler integration', function () { username: 'foo', password: 'bar', grant_type: 'password', - scope: 'baz', + scope: 'baz' }, headers: { 'content-type': 'application/x-www-form-urlencoded', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: 'POST', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -516,7 +516,7 @@ describe('TokenHandler integration', function () { refreshToken: 'bar', scope: ['baz'], user: {}, - foo: 'bar', + foo: 'bar' }; const model = Model.from({ getClient: function () { @@ -530,13 +530,13 @@ describe('TokenHandler integration', function () { }, validateScope: function () { return ['baz']; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, - allowExtendedTokenAttributes: true, + allowExtendedTokenAttributes: true }); const request = new Request({ body: { @@ -545,14 +545,14 @@ describe('TokenHandler integration', function () { username: 'foo', password: 'bar', grant_type: 'password', - scope: 'baz', + scope: 'baz' }, headers: { 'content-type': 'application/x-www-form-urlencoded', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: 'POST', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -573,18 +573,18 @@ describe('TokenHandler integration', function () { it('should throw an error if `clientId` is invalid', async function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 'øå€£‰', client_secret: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -600,18 +600,18 @@ describe('TokenHandler integration', function () { it('should throw an error if `clientSecret` is invalid', async function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 'foo', client_secret: 'øå€£‰' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -627,18 +627,18 @@ describe('TokenHandler integration', function () { it('should throw an error if `client` is missing', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -655,18 +655,18 @@ describe('TokenHandler integration', function () { getClient: function () { return {}; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -683,18 +683,18 @@ describe('TokenHandler integration', function () { getClient: function () { return { grants: 'foobar' }; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -709,20 +709,20 @@ describe('TokenHandler integration', function () { it('should throw a 401 error if the client is invalid and the request contains an authorization header', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: { - authorization: util.format('Basic %s', Buffer.from('foo:bar').toString('base64')), + authorization: util.format('Basic %s', Buffer.from('foo:bar').toString('base64')) }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -744,18 +744,18 @@ describe('TokenHandler integration', function () { getClient: function () { return client; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -773,7 +773,7 @@ describe('TokenHandler integration', function () { getClient: function () { return client; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ @@ -781,14 +781,14 @@ describe('TokenHandler integration', function () { model: model, refreshTokenLifetime: 120, requireClientAuthentication: { - password: false, - }, + password: false + } }); const request = new Request({ body: { client_id: 'blah', grant_type: 'password' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -807,7 +807,7 @@ describe('TokenHandler integration', function () { getClient: function () { return client; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ @@ -815,16 +815,16 @@ describe('TokenHandler integration', function () { model: model, refreshTokenLifetime: 120, requireClientAuthentication: { - password: false, - }, + password: false + } }); const request = new Request({ body: { grant_type: 'password' }, headers: { - authorization: util.format('Basic %s', Buffer.from('blah:').toString('base64')), + authorization: util.format('Basic %s', Buffer.from('blah:').toString('base64')) }, method: {}, - query: {}, + query: {} }); return handler @@ -841,18 +841,18 @@ describe('TokenHandler integration', function () { getClient: async function () { return { grants: [] }; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, - query: {}, + query: {} }); handler.getClient(request).should.be.an.instanceOf(Promise); @@ -863,18 +863,18 @@ describe('TokenHandler integration', function () { getClient: function () { return { grants: [] }; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, - query: {}, + query: {} }); handler.getClient(request).should.be.an.instanceOf(Promise); @@ -885,18 +885,18 @@ describe('TokenHandler integration', function () { it('should throw an error if `client_id` is missing', async function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_secret: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -912,18 +912,18 @@ describe('TokenHandler integration', function () { it('should throw an error if `client_secret` is missing', async function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -940,19 +940,19 @@ describe('TokenHandler integration', function () { it('should return a client', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, - requireClientAuthentication: { password: false }, + requireClientAuthentication: { password: false } }); const request = new Request({ body: { client_id: 'foo', grant_type: 'password' }, headers: {}, method: {}, - query: {}, + query: {} }); const credentials = handler.getClientCredentials(request); @@ -964,20 +964,20 @@ describe('TokenHandler integration', function () { it('should return a client', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: { - authorization: util.format('Basic %s', Buffer.from('foo:bar').toString('base64')), + authorization: util.format('Basic %s', Buffer.from('foo:bar').toString('base64')) }, method: {}, - query: {}, + query: {} }); const credentials = handler.getClientCredentials(request); @@ -989,18 +989,18 @@ describe('TokenHandler integration', function () { it('should return a client', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 'foo', client_secret: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); const credentials = handler.getClientCredentials(request); @@ -1013,18 +1013,18 @@ describe('TokenHandler integration', function () { it('should throw an error if `grant_type` is missing', async function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1040,18 +1040,18 @@ describe('TokenHandler integration', function () { it('should throw an error if `grant_type` is invalid', async function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: '~foo~' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1067,18 +1067,18 @@ describe('TokenHandler integration', function () { it('should throw an error if `grant_type` is unsupported', async function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: 'foobar' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1095,18 +1095,18 @@ describe('TokenHandler integration', function () { const client = { grants: ['client_credentials'] }; const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: 'password' }, headers: {}, method: {}, - query: {}, + query: {} }); try { @@ -1126,18 +1126,18 @@ describe('TokenHandler integration', function () { return client; }, getUser: function (uid, pwd) {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: 'password', username: 'foo', password: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1159,7 +1159,7 @@ describe('TokenHandler integration', function () { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() * 2), - user: {}, + user: {} }; }, getClient: function () {}, @@ -1174,23 +1174,23 @@ describe('TokenHandler integration', function () { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { code: 12345, - grant_type: 'authorization_code', + grant_type: 'authorization_code' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1214,7 +1214,7 @@ describe('TokenHandler integration', function () { expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: method, - codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()), + codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; @@ -1231,22 +1231,22 @@ describe('TokenHandler integration', function () { }, revokeAuthorizationCode: function () { return authorizationCode; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { code: 12345, grant_type: 'authorization_code', - code_verifier: codeVerifier, + code_verifier: codeVerifier }, headers: {}, method: {}, - query: {}, + query: {} }); const data = await handler.handleGrantType(request, client); @@ -1265,7 +1265,7 @@ describe('TokenHandler integration', function () { expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: method, - codeChallenge: codeVerifier, + codeChallenge: codeVerifier }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; @@ -1282,23 +1282,23 @@ describe('TokenHandler integration', function () { }, revokeAuthorizationCode: function () { return authorizationCode; - }, + } }); const handler = new TokenHandler({ enablePlainPKCE: true, accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { code: 12345, grant_type: 'authorization_code', - code_verifier: codeVerifier, + code_verifier: codeVerifier }, headers: {}, method: {}, - query: {}, + query: {} }); const data = await handler.handleGrantType(request, client); @@ -1314,7 +1314,7 @@ describe('TokenHandler integration', function () { expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: 'S256', - codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()), + codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; @@ -1331,22 +1331,22 @@ describe('TokenHandler integration', function () { }, revokeAuthorizationCode: function () { return authorizationCode; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { code: 12345, grant_type: 'authorization_code', - code_verifier: '123123123123123123123123123123123123123123123', + code_verifier: '123123123123123123123123123123123123123123123' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1366,7 +1366,7 @@ describe('TokenHandler integration', function () { expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: 'S256', - codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()), + codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; @@ -1383,21 +1383,21 @@ describe('TokenHandler integration', function () { }, revokeAuthorizationCode: function () { return authorizationCode; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { code: 12345, - grant_type: 'authorization_code', + grant_type: 'authorization_code' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1414,7 +1414,7 @@ describe('TokenHandler integration', function () { authorizationCode: 12345, client: { id: 'foobar' }, expiresAt: new Date(new Date().getTime() * 2), - user: {}, + user: {} }; const client = { id: 'foobar', grants: ['authorization_code'] }; const token = {}; @@ -1431,22 +1431,22 @@ describe('TokenHandler integration', function () { }, revokeAuthorizationCode: function () { return authorizationCode; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { code: 12345, grant_type: 'authorization_code', - code_verifier: '123123123123123123123123123123123123123123123', + code_verifier: '123123123123123123123123123123123123123123123' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1473,21 +1473,21 @@ describe('TokenHandler integration', function () { }, validateScope: function () { return ['foo']; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: 'client_credentials', - scope: 'foo', + scope: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1513,12 +1513,12 @@ describe('TokenHandler integration', function () { }, validateScope: function () { return ['baz']; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { @@ -1527,11 +1527,11 @@ describe('TokenHandler integration', function () { grant_type: 'password', password: 'bar', username: 'foo', - scope: 'baz', + scope: 'baz' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1554,7 +1554,7 @@ describe('TokenHandler integration', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() * 2), - user: {}, + user: {} }; }, saveToken: function () { @@ -1565,23 +1565,23 @@ describe('TokenHandler integration', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, + user: {} }; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { grant_type: 'refresh_token', - refresh_token: 12345, + refresh_token: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1596,7 +1596,7 @@ describe('TokenHandler integration', function () { describe('with custom grant_type', function () { it('should return a token', function () { const client = { - grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'], + grants: ['urn:ietf:params:oauth:grant-type:saml2-bearer'] }; const token = {}; const model = Model.from({ @@ -1609,25 +1609,25 @@ describe('TokenHandler integration', function () { }, validateScope: function () { return ['foo']; - }, + } }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120, extendedGrantTypes: { - 'urn:ietf:params:oauth:grant-type:saml2-bearer': PasswordGrantType, - }, + 'urn:ietf:params:oauth:grant-type:saml2-bearer': PasswordGrantType + } }); const request = new Request({ body: { grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer', username: 'foo', - password: 'bar', + password: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -1647,12 +1647,12 @@ describe('TokenHandler integration', function () { getClient: function () { return client; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); handler.getAccessTokenLifetime(client).should.equal(60); @@ -1664,12 +1664,12 @@ describe('TokenHandler integration', function () { getClient: function () { return client; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); handler.getAccessTokenLifetime(client).should.equal(120); @@ -1683,12 +1683,12 @@ describe('TokenHandler integration', function () { getClient: function () { return client; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); handler.getRefreshTokenLifetime(client).should.equal(60); @@ -1700,12 +1700,12 @@ describe('TokenHandler integration', function () { getClient: function () { return client; }, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); handler.getRefreshTokenLifetime(client).should.equal(120); @@ -1716,23 +1716,23 @@ describe('TokenHandler integration', function () { it('should return a token type', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const tokenType = handler.getTokenType({ accessToken: 'foo', refreshToken: 'bar', - scope: ['foobar'], + scope: ['foobar'] }); tokenType.should.deep.include({ accessToken: 'foo', accessTokenLifetime: undefined, refreshToken: 'bar', - scope: ['foobar'], + scope: ['foobar'] }); }); }); @@ -1741,12 +1741,12 @@ describe('TokenHandler integration', function () { it('should set the `body`', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const tokenType = new BearerTokenType('foo', 'bar', 'biz'); const response = new Response({ body: {}, headers: {} }); @@ -1757,19 +1757,19 @@ describe('TokenHandler integration', function () { access_token: 'foo', expires_in: 'bar', refresh_token: 'biz', - token_type: 'Bearer', + token_type: 'Bearer' }); }); it('should set the `Cache-Control` header', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const tokenType = new BearerTokenType('foo', 'bar', 'biz'); const response = new Response({ body: {}, headers: {} }); @@ -1782,12 +1782,12 @@ describe('TokenHandler integration', function () { it('should set the `Pragma` header', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const tokenType = new BearerTokenType('foo', 'bar', 'biz'); const response = new Response({ body: {}, headers: {} }); @@ -1803,12 +1803,12 @@ describe('TokenHandler integration', function () { const error = new AccessDeniedError('Cannot request a token'); const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const response = new Response({ body: {}, headers: {} }); @@ -1822,12 +1822,12 @@ describe('TokenHandler integration', function () { const error = new AccessDeniedError('Cannot request a token'); const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const response = new Response({ body: {}, headers: {} }); diff --git a/test/integration/request_test.js b/test/integration/request_test.js index 7766603f..76dbd6d4 100644 --- a/test/integration/request_test.js +++ b/test/integration/request_test.js @@ -52,7 +52,7 @@ describe('Request integration', function () { body: 'foo', headers: {}, method: {}, - query: {}, + query: {} }); request.body.should.equal('foo'); @@ -63,7 +63,7 @@ describe('Request integration', function () { body: {}, headers: { foo: 'bar', QuX: 'biz' }, method: {}, - query: {}, + query: {} }); request.headers.should.eql({ foo: 'bar', qux: 'biz' }); @@ -74,7 +74,7 @@ describe('Request integration', function () { body: {}, headers: {}, method: 'biz', - query: {}, + query: {} }); request.method.should.equal('biz'); @@ -85,7 +85,7 @@ describe('Request integration', function () { body: {}, headers: {}, method: {}, - query: 'baz', + query: 'baz' }); request.query.should.equal('baz'); @@ -98,7 +98,7 @@ describe('Request integration', function () { body: {}, headers: {}, method: {}, - query: {}, + query: {} }); (undefined === request.get('content-type')).should.be.true; @@ -108,10 +108,10 @@ describe('Request integration', function () { const request = new Request({ body: {}, headers: { - 'content-type': 'text/html; charset=utf-8', + 'content-type': 'text/html; charset=utf-8' }, method: {}, - query: {}, + query: {} }); request.get('Content-Type').should.equal('text/html; charset=utf-8'); @@ -124,10 +124,10 @@ describe('Request integration', function () { body: {}, headers: { 'content-type': 'application/json', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: {}, - query: {}, + query: {} }); request.is(['html', 'json']).should.equal('json'); @@ -138,10 +138,10 @@ describe('Request integration', function () { body: {}, headers: { 'content-type': 'application/json', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: {}, - query: {}, + query: {} }); request.is('html', 'json').should.equal('json'); @@ -152,10 +152,10 @@ describe('Request integration', function () { body: {}, headers: { 'content-type': 'text/html; charset=utf-8', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: {}, - query: {}, + query: {} }); request.is('html').should.equal('html'); @@ -166,10 +166,10 @@ describe('Request integration', function () { body: {}, headers: { 'content-type': 'text/html; charset=utf-8', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: {}, - query: {}, + query: {} }); request.is('json').should.be.false; @@ -180,7 +180,7 @@ describe('Request integration', function () { body: {}, headers: {}, method: {}, - query: {}, + query: {} }); request.is('text/html').should.be.false; diff --git a/test/integration/response_test.js b/test/integration/response_test.js index cbaf529f..7b074b97 100644 --- a/test/integration/response_test.js +++ b/test/integration/response_test.js @@ -21,7 +21,7 @@ describe('Response integration', function () { it('should set the `headers`', function () { const response = new Response({ body: {}, - headers: { foo: 'bar', QuX: 'biz' }, + headers: { foo: 'bar', QuX: 'biz' } }); response.headers.should.eql({ foo: 'bar', qux: 'biz' }); @@ -44,7 +44,7 @@ describe('Response integration', function () { it('should return the value if the field exists', function () { const response = new Response({ body: {}, - headers: { 'content-type': 'text/html; charset=utf-8' }, + headers: { 'content-type': 'text/html; charset=utf-8' } }); response.get('Content-Type').should.equal('text/html; charset=utf-8'); diff --git a/test/integration/server_test.js b/test/integration/server_test.js index 53617806..c9360086 100644 --- a/test/integration/server_test.js +++ b/test/integration/server_test.js @@ -44,16 +44,16 @@ describe('Server integration', function () { getAccessToken: function () { return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; - }, + } }); const server = new Server({ model: model }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -72,16 +72,16 @@ describe('Server integration', function () { getAccessToken: async function (token) { return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; - }, + } }); const server = new Server({ model: model }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); const handler = server.authenticate(request, response); @@ -96,29 +96,29 @@ describe('Server integration', function () { getAccessToken: function () { return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: function () { return { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function () { return { authorizationCode: 123 }; - }, + } }); const server = new Server({ model: model }); const request = new Request({ body: { client_id: 1234, client_secret: 'secret', - response_type: 'code', + response_type: 'code' }, headers: { Authorization: 'Bearer foo' }, method: {}, - query: { state: 'foobar' }, + query: { state: 'foobar' } }); const response = new Response({ body: {}, headers: {} }); @@ -136,29 +136,29 @@ describe('Server integration', function () { getAccessToken: function () { return { user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }; }, getClient: function () { return { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; }, saveAuthorizationCode: function () { return { authorizationCode: 123 }; - }, + } }); const server = new Server({ model: model }); const request = new Request({ body: { client_id: 1234, client_secret: 'secret', - response_type: 'code', + response_type: 'code' }, headers: { Authorization: 'Bearer foo' }, method: {}, - query: { state: 'foobar' }, + query: { state: 'foobar' } }); const response = new Response({ body: {}, headers: {} }); const handler = server.authorize(request, response); @@ -181,7 +181,7 @@ describe('Server integration', function () { }, validateScope: function () { return ['foo']; - }, + } }); const server = new Server({ model: model }); const request = new Request({ @@ -191,14 +191,14 @@ describe('Server integration', function () { grant_type: 'password', username: 'foo', password: 'pass', - scope: 'foo', + scope: 'foo' }, headers: { 'content-type': 'application/x-www-form-urlencoded', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: 'POST', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); @@ -221,7 +221,7 @@ describe('Server integration', function () { }, saveToken: function () { return { accessToken: 1234, client: {}, user: {} }; - }, + } }); const server = new Server({ model: model }); const request = new Request({ @@ -230,14 +230,14 @@ describe('Server integration', function () { client_secret: 'secret', grant_type: 'password', username: 'foo', - password: 'pass', + password: 'pass' }, headers: { 'content-type': 'application/x-www-form-urlencoded', - 'transfer-encoding': 'chunked', + 'transfer-encoding': 'chunked' }, method: 'POST', - query: {}, + query: {} }); const response = new Response({ body: {}, headers: {} }); const handler = server.token(request, response); diff --git a/test/integration/token-types/bearer-token-type_test.js b/test/integration/token-types/bearer-token-type_test.js index 0b87d84d..68d95879 100644 --- a/test/integration/token-types/bearer-token-type_test.js +++ b/test/integration/token-types/bearer-token-type_test.js @@ -52,7 +52,7 @@ describe('BearerTokenType integration', function () { value.should.eql({ access_token: 'foo', expires_in: 'bar', - token_type: 'Bearer', + token_type: 'Bearer' }); }); @@ -62,7 +62,7 @@ describe('BearerTokenType integration', function () { value.should.eql({ access_token: 'foo', - token_type: 'Bearer', + token_type: 'Bearer' }); }); @@ -74,7 +74,7 @@ describe('BearerTokenType integration', function () { access_token: 'foo', expires_in: 'bar', refresh_token: 'biz', - token_type: 'Bearer', + token_type: 'Bearer' }); }); @@ -86,7 +86,7 @@ describe('BearerTokenType integration', function () { access_token: 'foo', expires_in: 'bar', refresh_token: 'biz', - token_type: 'Bearer', + token_type: 'Bearer' }); }); }); diff --git a/test/unit/grant-types/abstract-grant-type_test.js b/test/unit/grant-types/abstract-grant-type_test.js index 24cc6292..0a1eb0c7 100644 --- a/test/unit/grant-types/abstract-grant-type_test.js +++ b/test/unit/grant-types/abstract-grant-type_test.js @@ -17,11 +17,11 @@ describe('AbstractGrantType', function () { describe('generateAccessToken()', function () { it('should call `model.generateAccessToken()`', function () { const model = Model.from({ - generateAccessToken: sinon.stub().returns({ client: {}, expiresAt: new Date(), user: {} }), + generateAccessToken: sinon.stub().returns({ client: {}, expiresAt: new Date(), user: {} }) }); const handler = new AbstractGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); return handler @@ -40,12 +40,12 @@ describe('AbstractGrantType', function () { generateRefreshToken: sinon.stub().returns({ client: {}, expiresAt: new Date(new Date() / 2), - user: {}, - }), + user: {} + }) }); const handler = new AbstractGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); return handler diff --git a/test/unit/grant-types/authorization-code-grant-type_test.js b/test/unit/grant-types/authorization-code-grant-type_test.js index e723e58a..089419a2 100644 --- a/test/unit/grant-types/authorization-code-grant-type_test.js +++ b/test/unit/grant-types/authorization-code-grant-type_test.js @@ -24,20 +24,20 @@ describe('AuthorizationCodeGrantType', function () { authorizationCode: 12345, client: {}, expiresAt: new Date(new Date() * 2), - user: {}, + user: {} }), revokeAuthorizationCode: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: { code: 12345 }, headers: {}, method: {}, - query: {}, + query: {} }); const client = {}; @@ -58,11 +58,11 @@ describe('AuthorizationCodeGrantType', function () { const model = Model.from({ getAuthorizationCode: function () {}, revokeAuthorizationCode: sinon.stub().returns(true), - saveToken: function () {}, + saveToken: function () {} }); const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const authorizationCode = {}; @@ -85,11 +85,11 @@ describe('AuthorizationCodeGrantType', function () { const model = Model.from({ getAuthorizationCode: function () {}, revokeAuthorizationCode: function () {}, - saveToken: sinon.stub().returns(true), + saveToken: sinon.stub().returns(true) }); const handler = new AuthorizationCodeGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); sinon.stub(handler, 'validateScope').returns(['foobiz']); @@ -109,7 +109,7 @@ describe('AuthorizationCodeGrantType', function () { accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', - scope: ['foobiz'], + scope: ['foobiz'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); @@ -131,7 +131,7 @@ describe('AuthorizationCodeGrantType', function () { expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: 'S256', - codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()), + codeChallenge: stringUtil.base64URLEncode(crypto.createHash('sha256').update(codeVerifier).digest()) }; const client = { id: 'foobar', isPublic: true }; const model = Model.from({ @@ -139,17 +139,17 @@ describe('AuthorizationCodeGrantType', function () { return authorizationCode; }, revokeAuthorizationCode: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345, code_verifier: codeVerifier }, headers: {}, method: {}, - query: {}, + query: {} }); return grantType @@ -168,7 +168,7 @@ describe('AuthorizationCodeGrantType', function () { expiresAt: new Date(new Date().getTime() * 2), user: {}, codeChallengeMethod: 'plain', - codeChallenge: codeVerifier, + codeChallenge: codeVerifier }; const client = { id: 'foobar', isPublic: true }; const model = Model.from({ @@ -176,17 +176,17 @@ describe('AuthorizationCodeGrantType', function () { return authorizationCode; }, revokeAuthorizationCode: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const grantType = new AuthorizationCodeGrantType({ accessTokenLifetime: 123, - model: model, + model: model }); const request = new Request({ body: { code: 12345, code_verifier: codeVerifier }, headers: {}, method: {}, - query: {}, + query: {} }); return grantType diff --git a/test/unit/grant-types/client-credentials-grant-type_test.js b/test/unit/grant-types/client-credentials-grant-type_test.js index fb3042dd..1939bfff 100644 --- a/test/unit/grant-types/client-credentials-grant-type_test.js +++ b/test/unit/grant-types/client-credentials-grant-type_test.js @@ -18,11 +18,11 @@ describe('ClientCredentialsGrantType', function () { it('should call `model.getUserFromClient()`', function () { const model = Model.from({ getUserFromClient: sinon.stub().returns(true), - saveToken: function () {}, + saveToken: function () {} }); const handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const client = {}; @@ -44,11 +44,11 @@ describe('ClientCredentialsGrantType', function () { const user = {}; const model = Model.from({ getUserFromClient: function () {}, - saveToken: sinon.stub().returns(true), + saveToken: sinon.stub().returns(true) }); const handler = new ClientCredentialsGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); sinon.stub(handler, 'validateScope').returns(['foobar']); @@ -63,7 +63,7 @@ describe('ClientCredentialsGrantType', function () { model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', - scope: ['foobar'], + scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); diff --git a/test/unit/grant-types/password-grant-type_test.js b/test/unit/grant-types/password-grant-type_test.js index d2e31ce9..ad0f14c3 100644 --- a/test/unit/grant-types/password-grant-type_test.js +++ b/test/unit/grant-types/password-grant-type_test.js @@ -19,18 +19,18 @@ describe('PasswordGrantType', function () { it('should call `model.getUser()`', function () { const model = Model.from({ getUser: sinon.stub().returns(true), - saveToken: function () {}, + saveToken: function () {} }); const client = { id: 'foobar' }; const handler = new PasswordGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: { username: 'foo', password: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -52,11 +52,11 @@ describe('PasswordGrantType', function () { const user = {}; const model = Model.from({ getUser: function () {}, - saveToken: sinon.stub().returns(true), + saveToken: sinon.stub().returns(true) }); const handler = new PasswordGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); sinon.stub(handler, 'validateScope').returns(['foobar']); @@ -75,7 +75,7 @@ describe('PasswordGrantType', function () { accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', - scope: ['foobar'], + scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); diff --git a/test/unit/grant-types/refresh-token-grant-type_test.js b/test/unit/grant-types/refresh-token-grant-type_test.js index 92d2dfc5..2f5b0ad9 100644 --- a/test/unit/grant-types/refresh-token-grant-type_test.js +++ b/test/unit/grant-types/refresh-token-grant-type_test.js @@ -29,18 +29,18 @@ describe('RefreshTokenGrantType', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, - }), + user: {} + }) }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); const client = {}; @@ -61,17 +61,17 @@ describe('RefreshTokenGrantType', function () { const model = Model.from({ getRefreshToken: sinon.stub().returns({ accessToken: 'foo', client: {}, user: {} }), saveToken: function () {}, - revokeToken: function () {}, + revokeToken: function () {} }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const request = new Request({ body: { refresh_token: 'bar' }, headers: {}, method: {}, - query: {}, + query: {} }); const client = {}; @@ -95,13 +95,13 @@ describe('RefreshTokenGrantType', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, + user: {} }), - saveToken: function () {}, + saveToken: function () {} }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); const token = {}; @@ -123,14 +123,14 @@ describe('RefreshTokenGrantType', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, + user: {} }), - saveToken: function () {}, + saveToken: function () {} }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, - alwaysIssueNewRefreshToken: false, + alwaysIssueNewRefreshToken: false }); const token = {}; @@ -149,14 +149,14 @@ describe('RefreshTokenGrantType', function () { accessToken: 'foo', client: {}, refreshTokenExpiresAt: new Date(new Date() / 2), - user: {}, + user: {} }), - saveToken: function () {}, + saveToken: function () {} }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, - alwaysIssueNewRefreshToken: true, + alwaysIssueNewRefreshToken: true }); const token = {}; @@ -179,11 +179,11 @@ describe('RefreshTokenGrantType', function () { const model = Model.from({ getRefreshToken: function () {}, revokeToken: function () {}, - saveToken: sinon.stub().returns(true), + saveToken: sinon.stub().returns(true) }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, - model: model, + model: model }); sinon.stub(handler, 'generateAccessToken').returns('foo'); @@ -201,7 +201,7 @@ describe('RefreshTokenGrantType', function () { accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', - scope: ['foobar'], + scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); @@ -216,12 +216,12 @@ describe('RefreshTokenGrantType', function () { const model = Model.from({ getRefreshToken: function () {}, revokeToken: function () {}, - saveToken: sinon.stub().returns(true), + saveToken: sinon.stub().returns(true) }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, - alwaysIssueNewRefreshToken: false, + alwaysIssueNewRefreshToken: false }); sinon.stub(handler, 'generateAccessToken').returns('foo'); @@ -237,7 +237,7 @@ describe('RefreshTokenGrantType', function () { model.saveToken.firstCall.args[0].should.eql({ accessToken: 'foo', accessTokenExpiresAt: 'biz', - scope: ['foobar'], + scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); @@ -252,12 +252,12 @@ describe('RefreshTokenGrantType', function () { const model = Model.from({ getRefreshToken: function () {}, revokeToken: function () {}, - saveToken: sinon.stub().returns(true), + saveToken: sinon.stub().returns(true) }); const handler = new RefreshTokenGrantType({ accessTokenLifetime: 120, model: model, - alwaysIssueNewRefreshToken: true, + alwaysIssueNewRefreshToken: true }); sinon.stub(handler, 'generateAccessToken').returns('foo'); @@ -275,7 +275,7 @@ describe('RefreshTokenGrantType', function () { accessTokenExpiresAt: 'biz', refreshToken: 'bar', refreshTokenExpiresAt: 'baz', - scope: ['foobar'], + scope: ['foobar'] }); model.saveToken.firstCall.args[1].should.equal(client); model.saveToken.firstCall.args[2].should.equal(user); diff --git a/test/unit/handlers/authenticate-handler_test.js b/test/unit/handlers/authenticate-handler_test.js index fa1a38fb..fdb64251 100644 --- a/test/unit/handlers/authenticate-handler_test.js +++ b/test/unit/handlers/authenticate-handler_test.js @@ -21,15 +21,15 @@ describe('AuthenticateHandler', function () { describe('with bearer token in the request authorization header', function () { it('should throw an error if the token is malformed', () => { const handler = new AuthenticateHandler({ - model: { getAccessToken() {} }, + model: { getAccessToken() {} } }); const request = new Request({ body: {}, headers: { - Authorization: 'foo Bearer bar', + Authorization: 'foo Bearer bar' }, method: 'ANY', - query: {}, + query: {} }); try { @@ -46,13 +46,13 @@ describe('AuthenticateHandler', function () { describe('with bearer token in the request authorization header', function () { it('should call `getTokenFromRequestHeader()`', function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: {}, headers: { Authorization: 'Bearer foo' }, method: {}, - query: {}, + query: {} }); sinon.stub(handler, 'getTokenFromRequestHeader'); @@ -68,13 +68,13 @@ describe('AuthenticateHandler', function () { describe('with bearer token in the request query', function () { it('should call `getTokenFromRequestQuery()`', function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: {}, headers: {}, method: {}, - query: { access_token: 'foo' }, + query: { access_token: 'foo' } }); sinon.stub(handler, 'getTokenFromRequestQuery'); @@ -90,13 +90,13 @@ describe('AuthenticateHandler', function () { describe('with bearer token in the request body', function () { it('should call `getTokenFromRequestBody()`', function () { const handler = new AuthenticateHandler({ - model: { getAccessToken: function () {} }, + model: { getAccessToken: function () {} } }); const request = new Request({ body: { access_token: 'foo' }, headers: {}, method: {}, - query: {}, + query: {} }); sinon.stub(handler, 'getTokenFromRequestBody'); @@ -113,7 +113,7 @@ describe('AuthenticateHandler', function () { describe('getAccessToken()', function () { it('should call `model.getAccessToken()`', function () { const model = Model.from({ - getAccessToken: sinon.stub().returns({ user: {} }), + getAccessToken: sinon.stub().returns({ user: {} }) }); const handler = new AuthenticateHandler({ model: model }); @@ -132,14 +132,14 @@ describe('AuthenticateHandler', function () { describe('validateAccessToken()', function () { it('should fail if token has no valid `accessTokenExpiresAt` date', function () { const model = Model.from({ - getAccessToken: function () {}, + getAccessToken: function () {} }); const handler = new AuthenticateHandler({ model: model }); let failed = false; try { handler.validateAccessToken({ - user: {}, + user: {} }); } catch (err) { err.should.be.an.instanceOf(ServerError); @@ -150,13 +150,13 @@ describe('AuthenticateHandler', function () { it('should succeed if token has valid `accessTokenExpiresAt` date', function () { const model = Model.from({ - getAccessToken: function () {}, + getAccessToken: function () {} }); const handler = new AuthenticateHandler({ model: model }); try { handler.validateAccessToken({ user: {}, - accessTokenExpiresAt: new Date(new Date().getTime() + 10000), + accessTokenExpiresAt: new Date(new Date().getTime() + 10000) }); } catch (err) { should.fail(); @@ -168,13 +168,13 @@ describe('AuthenticateHandler', function () { it('should call `model.getAccessToken()` if scope is defined', function () { const model = Model.from({ getAccessToken: function () {}, - verifyScope: sinon.stub().returns(true), + verifyScope: sinon.stub().returns(true) }); const handler = new AuthenticateHandler({ addAcceptedScopesHeader: true, addAuthorizedScopesHeader: true, model: model, - scope: 'bar', + scope: 'bar' }); return handler diff --git a/test/unit/handlers/authorize-handler_test.js b/test/unit/handlers/authorize-handler_test.js index 56557c18..fb4170db 100644 --- a/test/unit/handlers/authorize-handler_test.js +++ b/test/unit/handlers/authorize-handler_test.js @@ -22,11 +22,11 @@ describe('AuthorizeHandler', function () { generateAuthorizationCode: sinon.stub().returns({}), getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model: model, + model: model }); return handler @@ -45,19 +45,19 @@ describe('AuthorizeHandler', function () { getAccessToken: function () {}, getClient: sinon.stub().returns({ grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }), - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model: model, + model: model }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -75,22 +75,22 @@ describe('AuthorizeHandler', function () { describe('getUser()', function () { it('should call `authenticateHandler.getUser()`', function () { const authenticateHandler = { - handle: sinon.stub().returns(Promise.resolve({})), + handle: sinon.stub().returns(Promise.resolve({})) }; const model = Model.from({ getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const handler = new AuthorizeHandler({ authenticateHandler: authenticateHandler, authorizationCodeLifetime: 120, - model: model, + model: model }); const request = new Request({ body: {}, headers: {}, method: {}, - query: {}, + query: {} }); const response = new Response(); @@ -111,11 +111,11 @@ describe('AuthorizeHandler', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: sinon.stub().returns({}), + saveAuthorizationCode: sinon.stub().returns({}) }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model: model, + model: model }); return handler @@ -127,7 +127,7 @@ describe('AuthorizeHandler', function () { authorizationCode: 'foo', expiresAt: 'bar', redirectUri: 'baz', - scope: ['qux'], + scope: ['qux'] }); model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); @@ -140,11 +140,11 @@ describe('AuthorizeHandler', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: sinon.stub().returns({}), + saveAuthorizationCode: sinon.stub().returns({}) }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model: model, + model: model }); return handler @@ -158,7 +158,7 @@ describe('AuthorizeHandler', function () { redirectUri: 'baz', scope: ['qux'], codeChallenge: 'codeChallenge', - codeChallengeMethod: 'codeChallengeMethod', + codeChallengeMethod: 'codeChallengeMethod' }); model.saveAuthorizationCode.firstCall.args[1].should.equal('biz'); model.saveAuthorizationCode.firstCall.args[2].should.equal('boz'); @@ -172,24 +172,24 @@ describe('AuthorizeHandler', function () { it('should call `model.validateRedirectUri()`', function () { const client = { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const redirect_uri = 'http://example.com/cb/2'; const model = Model.from({ getAccessToken: function () {}, getClient: sinon.stub().returns(client), saveAuthorizationCode: function () {}, - validateRedirectUri: sinon.stub().returns(true), + validateRedirectUri: sinon.stub().returns(true) }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model: model, + model: model }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, - query: {}, + query: {} }); return handler @@ -212,7 +212,7 @@ describe('AuthorizeHandler', function () { it('should be successful validation', function () { const client = { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const redirect_uri = 'http://example.com/cb'; const model = Model.from({ @@ -221,18 +221,18 @@ describe('AuthorizeHandler', function () { saveAuthorizationCode: function () {}, validateRedirectUri: function (redirectUri, client) { return client.redirectUris.includes(redirectUri); - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model: model, + model: model }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, - query: {}, + query: {} }); return handler.getClient(request).then((client) => { @@ -243,7 +243,7 @@ describe('AuthorizeHandler', function () { it('should be unsuccessful validation', function () { const client = { grants: ['authorization_code'], - redirectUris: ['http://example.com/cb'], + redirectUris: ['http://example.com/cb'] }; const redirect_uri = 'http://example.com/callback'; const model = Model.from({ @@ -252,18 +252,18 @@ describe('AuthorizeHandler', function () { saveAuthorizationCode: function () {}, validateRedirectUri: function (redirectUri, client) { return client.redirectUris.includes(redirectUri); - }, + } }); const handler = new AuthorizeHandler({ authorizationCodeLifetime: 120, - model: model, + model: model }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret', redirect_uri }, headers: {}, method: {}, - query: {}, + query: {} }); return handler diff --git a/test/unit/handlers/token-handler_test.js b/test/unit/handlers/token-handler_test.js index 89b30fb2..bf092824 100644 --- a/test/unit/handlers/token-handler_test.js +++ b/test/unit/handlers/token-handler_test.js @@ -19,18 +19,18 @@ describe('TokenHandler', function () { it('should call `model.getClient()`', function () { const model = Model.from({ getClient: sinon.stub().returns({ grants: ['password'] }), - saveToken: function () {}, + saveToken: function () {} }); const handler = new TokenHandler({ accessTokenLifetime: 120, model: model, - refreshTokenLifetime: 120, + refreshTokenLifetime: 120 }); const request = new Request({ body: { client_id: 12345, client_secret: 'secret' }, headers: {}, method: {}, - query: {}, + query: {} }); return handler diff --git a/test/unit/models/token-model_test.js b/test/unit/models/token-model_test.js index 2ac275df..bd8b6dfb 100644 --- a/test/unit/models/token-model_test.js +++ b/test/unit/models/token-model_test.js @@ -23,7 +23,7 @@ describe('TokenModel', function () { const data = { client: 'bar', user: 'tar', - accessTokenExpiresAt: atExpiresAt, + accessTokenExpiresAt: atExpiresAt }; try { @@ -41,7 +41,7 @@ describe('TokenModel', function () { const data = { accessToken: 'foo', user: 'tar', - accessTokenExpiresAt: atExpiresAt, + accessTokenExpiresAt: atExpiresAt }; try { @@ -59,7 +59,7 @@ describe('TokenModel', function () { const data = { accessToken: 'foo', client: 'bar', - accessTokenExpiresAt: atExpiresAt, + accessTokenExpiresAt: atExpiresAt }; try { @@ -75,7 +75,7 @@ describe('TokenModel', function () { accessToken: 'foo', client: 'bar', user: 'tar', - accessTokenExpiresAt: '11/10/2023', + accessTokenExpiresAt: '11/10/2023' }; try { @@ -91,7 +91,7 @@ describe('TokenModel', function () { accessToken: 'foo', client: 'bar', user: 'tar', - refreshTokenExpiresAt: '11/10/2023', + refreshTokenExpiresAt: '11/10/2023' }; try { @@ -110,7 +110,7 @@ describe('TokenModel', function () { accessToken: 'foo', client: 'bar', user: 'tar', - accessTokenExpiresAt: atExpiresAt, + accessTokenExpiresAt: atExpiresAt }; const model = new TokenModel(data); @@ -130,7 +130,7 @@ describe('TokenModel', function () { accessToken: 'token', client: 'client', user: 'user', - myCustomAttribute: 'myCustomValue', + myCustomAttribute: 'myCustomValue' }); should.not.exist(model['myCustomAttribute']); @@ -143,10 +143,10 @@ describe('TokenModel', function () { accessToken: 'token', client: 'client', user: 'user', - myCustomAttribute: 'myCustomValue', + myCustomAttribute: 'myCustomValue' }, { - allowExtendedTokenAttributes: true, + allowExtendedTokenAttributes: true } ); diff --git a/test/unit/pkce/pkce_test.js b/test/unit/pkce/pkce_test.js index e2461ce3..cb945294 100644 --- a/test/unit/pkce/pkce_test.js +++ b/test/unit/pkce/pkce_test.js @@ -20,13 +20,13 @@ describe('PKCE', function () { [false, 'foo_code', '123123123123123123123123123123123123123123123'], [false, '', '123123123123123123123123123123123123123123123'], [false, undefined, '123123123123123123123123123123123123123123123'], - [false, 'foo_code', 'bar'], + [false, 'foo_code', 'bar'] ].forEach((triple) => { should.equal( triple[0], pkce.isPKCERequest({ grantType: triple[1], - codeVerifier: triple[2], + codeVerifier: triple[2] }) ); }); @@ -42,10 +42,10 @@ describe('PKCE', function () { [false, '123123123112312312311231231231123123123112+'], // invalid chars [ false, - '123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123', + '123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123112312312311231231231123123123' ], // too long // invalid chars - [true, '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'], + [true, '-_.~abcdefghijklmnopqrstuvwxyz0123456789ABCDEFHIJKLMNOPQRSTUVWXYZ'] ].forEach((pair) => { should.equal(pair[0], pkce.codeChallengeMatchesABNF(pair[1])); }); @@ -59,13 +59,13 @@ describe('PKCE', function () { [undefined, undefined, verifier], [undefined, null, verifier], [undefined, '', verifier], - [undefined, 'foo', verifier], + [undefined, 'foo', verifier] ].forEach((triple) => { should.equal( triple[0], pkce.getHashForCodeChallenge({ method: triple[1], - verifier: triple[2], + verifier: triple[2] }) ); }); @@ -77,13 +77,13 @@ describe('PKCE', function () { [undefined, 'plain', ''], [undefined, 'S256', ''], [undefined, 'plain', null], - [undefined, 'S256', null], + [undefined, 'S256', null] ].forEach((triple) => { should.equal( triple[0], pkce.getHashForCodeChallenge({ method: triple[1], - verifier: triple[2], + verifier: triple[2] }) ); }); diff --git a/test/unit/request_test.js b/test/unit/request_test.js index 4b5aa8bc..9b646ea5 100644 --- a/test/unit/request_test.js +++ b/test/unit/request_test.js @@ -15,15 +15,15 @@ const should = require('chai').should(); function generateBaseRequest() { return { query: { - foo: 'bar', + foo: 'bar' }, method: 'GET', headers: { - bar: 'foo', + bar: 'foo' }, body: { - foobar: 'barfoo', - }, + foobar: 'barfoo' + } }; } @@ -34,7 +34,7 @@ describe('Request', function () { [null, TypeError, "Cannot destructure property 'headers'"], [{}, InvalidArgumentError, 'Missing parameter: `headers`'], [{ headers: {} }, InvalidArgumentError, 'Missing parameter: `method`'], - [{ headers: {}, method: 'GET' }, InvalidArgumentError, 'Missing parameter: `query`'], + [{ headers: {}, method: 'GET' }, InvalidArgumentError, 'Missing parameter: `query`'] ]; args.forEach(([value, error, message]) => { @@ -98,7 +98,7 @@ describe('Request', function () { const originalRequest = generateBaseRequest(); originalRequest.headers = { Foo: 'bar', - BAR: 'foo', + BAR: 'foo' }; const request = new Request(originalRequest); @@ -111,11 +111,11 @@ describe('Request', function () { it('should include additional properties passed in the request', function () { const originalRequest = generateBaseRequest(); originalRequest.custom = { - newFoo: 'newBar', + newFoo: 'newBar' }; originalRequest.custom2 = { - newBar: 'newFoo', + newBar: 'newFoo' }; const request = new Request(originalRequest); @@ -130,11 +130,11 @@ describe('Request', function () { it('should include additional properties passed in the request', function () { const originalRequest = generateBaseRequest(); originalRequest.custom = { - newFoo: 'newBar', + newFoo: 'newBar' }; originalRequest.custom2 = { - newBar: 'newFoo', + newBar: 'newFoo' }; const request = new Request(originalRequest); @@ -151,12 +151,12 @@ describe('Request', function () { query: {}, method: 'GET', headers: { - 'content-type': 'application/json', + 'content-type': 'application/json' }, get() { // malicious attempt to override the 'get' method return 'text/html'; - }, + } }); request.get('content-type').should.equal('application/json'); diff --git a/test/unit/response_test.js b/test/unit/response_test.js index 36dab673..9ad751f8 100644 --- a/test/unit/response_test.js +++ b/test/unit/response_test.js @@ -14,11 +14,11 @@ const should = require('chai').should(); function generateBaseResponse() { return { headers: { - bar: 'foo', + bar: 'foo' }, body: { - foobar: 'barfoo', - }, + foobar: 'barfoo' + } }; } @@ -56,7 +56,7 @@ describe('Request', function () { const originalResponse = generateBaseResponse(); originalResponse.headers = { Foo: 'bar', - BAR: 'foo', + BAR: 'foo' }; const response = new Response(originalResponse); @@ -69,11 +69,11 @@ describe('Request', function () { it('should include additional properties passed in the response', function () { const originalResponse = generateBaseResponse(); originalResponse.custom = { - newFoo: 'newBar', + newFoo: 'newBar' }; originalResponse.custom2 = { - newBar: 'newFoo', + newBar: 'newFoo' }; const response = new Response(originalResponse); @@ -86,12 +86,12 @@ describe('Request', function () { it('should not allow overwriting methods on the Response prototype via custom properties', () => { const response = new Response({ headers: { - 'content-type': 'application/json', + 'content-type': 'application/json' }, get() { // malicious attempt to override the 'get' method return 'text/html'; - }, + } }); response.get('content-type').should.equal('application/json'); diff --git a/test/unit/server_test.js b/test/unit/server_test.js index c9b1b7fd..704e4407 100644 --- a/test/unit/server_test.js +++ b/test/unit/server_test.js @@ -19,7 +19,7 @@ describe('Server', function () { describe('authenticate()', function () { it('should call `handle`', function () { const model = Model.from({ - getAccessToken: function () {}, + getAccessToken: function () {} }); const server = new Server({ model: model }); @@ -38,7 +38,7 @@ describe('Server', function () { const model = Model.from({ getAccessToken: function () {}, getClient: function () {}, - saveAuthorizationCode: function () {}, + saveAuthorizationCode: function () {} }); const server = new Server({ model: model }); @@ -56,7 +56,7 @@ describe('Server', function () { it('should call `handle`', function () { const model = Model.from({ getClient: function () {}, - saveToken: function () {}, + saveToken: function () {} }); const server = new Server({ model: model }); diff --git a/test/unit/utils/scope-util_test.js b/test/unit/utils/scope-util_test.js index a25358c2..8309753f 100644 --- a/test/unit/utils/scope-util_test.js +++ b/test/unit/utils/scope-util_test.js @@ -35,7 +35,7 @@ describe(parseScope.name, () => { const values = [ ['foo', ['foo']], ['foo bar', ['foo', 'bar']], - ['foo bar', ['foo', 'bar']], + ['foo bar', ['foo', 'bar']] ]; values.forEach(([str, compare]) => { const parsed = parseScope(str);