From 44301f50c0fc9f713c6ce8610aedf3780ef51464 Mon Sep 17 00:00:00 2001 From: Evan Thomas Date: Wed, 17 Jul 2024 18:25:53 -0400 Subject: [PATCH] [PLATFORM-3432] Update fork with upstream (v1.1.3) (#7) * set lifetime on creation * cleanup docs * allow loginSync response to set values in state * loginSync sets extra props in state * cookie test * more cookie tests * upgrade packages and cookie test * comment * ignore new output from playwright * 1.1 fix lougout, pass through client_id, redirect_id * 1.1.0 * fix docker command for v27 * only check if client_id was set earlier * docs update * 1.1.1 * fix setting client_id * 1.1.2 * use target_uri rather than redirect_uri * 1.1.3 * add puml sequence diagrams * chore: Pin node engine version --------- Co-authored-by: Dick Hardt --- .gitignore | 3 +- README.md | 33 ++- package-lock.json | 495 ++++++++++++++++---------------- package.json | 8 +- playwright/sync/sync.js | 14 +- playwright/tests/server.test.ts | 148 +++++++++- sequence/API-call.puml | 32 +++ sequence/login-flow-sdk.puml | 83 ++++++ sequence/logout.puml | 36 +++ src/api.ts | 209 +++++++++----- src/constants.ts | 1 + src/state.ts | 48 +++- tests/cookie-token.spec.ts | 13 +- tests/refresh-token.spec.ts | 8 +- 14 files changed, 765 insertions(+), 366 deletions(-) create mode 100644 sequence/API-call.puml create mode 100644 sequence/login-flow-sdk.puml create mode 100644 sequence/logout.puml diff --git a/.gitignore b/.gitignore index 3d44c04..9dd3973 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode node_modules /dist -playwright-report \ No newline at end of file +playwright-report +/test-results \ No newline at end of file diff --git a/README.md b/README.md index 81658e0..76d3f65 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# AS4Mobile +# client_as + Authorization Server for Mobile Apps ## Environment Configuration @@ -56,6 +57,7 @@ User is logged in. access_token and refresh_token cookies have been created and After the user has successfully logged in, call +``` POST /token HTTP/1.1 Host: app.tiltingpoint.com Content-Type: application/x-www-form-urlencoded @@ -64,14 +66,20 @@ DPoP: zzzzz grant_type=authorization_code& client_id=SDK-1.0.0 code= +``` +will return +``` { "access_token": "xxx", "token_type": "DPoP", "refresh_token": "yyy", "expires_in": 300 } +``` +Refreshing an access token +``` POST /token HTTP/1.1 Host: app.tiltingpoint.com Content-Type: application/x-www-form-urlencoded @@ -80,24 +88,33 @@ DPoP: zzzzz Refresh grant_type=refresh_token& refresh_token=yyy +``` +## Endpoints -Endpoints - -/token // public +### /token // public grant_type="cookie_token" device_info ??? -/jwks +### /jwks -/revoke +### /revoke -/.wellknown/oauth-authorization-server +### /.wellknown/oauth-authorization-server -/login +### /login - called by client after successful login +## Development + +- clone repo +- `npm i` to install all node modules +- `npx playwright install` to install Playwright binaries to test with + +`npm test` will run fastify.inject() tests + +`npm run playwright` will start all the services with docker compose and then run the Playwright tests for browser interactions \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 71cb1f1..dea8cf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "client-as", - "version": "1.0.14", + "version": "1.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "client-as", - "version": "1.0.14", + "version": "1.1.3", "license": "ISC", "dependencies": { "@fastify/formbody": "^7.4.0", - "@hellocoop/fastify": "latest", + "@hellocoop/fastify": "^1.11.5", + "@hellocoop/router": "^1.9.4", "cookie": "^0.6.0", "fastify": "^4.26.2", "ioredis": "^5.3.2", @@ -31,6 +32,9 @@ "rimraf": "^5.0.5", "ts-node": "^10.9.2", "typescript": "^5.4.2" + }, + "engines": { + "node": ">=18" } }, "node_modules/@cspotcode/source-map-support": { @@ -46,9 +50,9 @@ } }, "node_modules/@fastify/ajv-compiler": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", - "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.6.0.tgz", + "integrity": "sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==", "dependencies": { "ajv": "^8.11.0", "ajv-formats": "^2.1.1", @@ -86,30 +90,30 @@ } }, "node_modules/@hellocoop/constants": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@hellocoop/constants/-/constants-1.1.3.tgz", - "integrity": "sha512-3pUUY6aUPVPx+uyG9g8KFRj8uy1uwbrOyxR3nCGA1Z09+W3yqGD1gaD6esSqKB3VucR+YUFvSL3WSdnYfltzgA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@hellocoop/constants/-/constants-1.1.5.tgz", + "integrity": "sha512-YRRCK0M5iCI6lvJB8CZx+OIzy9f9amyrDs8od95zraHOAGqZTUy1mONSYJAIBsm8CqcY8RKPtE16zaDgRJd6Kw==", "engines": { "node": ">=18" } }, "node_modules/@hellocoop/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@hellocoop/core/-/core-1.4.3.tgz", - "integrity": "sha512-gCNmOeEsB0DIvgCjo6P7PhPvvszImxL5inIaKCmwX2qR6b5W1Mc3qQU/rY4L1jK8vZpbClTuwolCJMrK/2jIsw==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@hellocoop/core/-/core-1.4.5.tgz", + "integrity": "sha512-E7LiPkx9MxwHaGryKVSpNRX6VAezaWqgDN1Qm1KickfIAqRRGanlSy8H1SwsCnHEQhLQdKgPafXfzd3ao+eVcA==", "dependencies": { - "@hellocoop/constants": "*" + "@hellocoop/constants": "~1.1.5" }, "engines": { "node": ">=18" } }, "node_modules/@hellocoop/fastify": { - "version": "1.10.4", - "resolved": "https://registry.npmjs.org/@hellocoop/fastify/-/fastify-1.10.4.tgz", - "integrity": "sha512-bBflSi1TpeqFwM23RdPXWWGxuSSxcmv51u2g3V+VGIuWBQPw3FZT8ovf3LNmcY4gLwgz5VreK4uDYmk3WGW/OA==", + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@hellocoop/fastify/-/fastify-1.11.5.tgz", + "integrity": "sha512-PdpR3qr1lF39n4ujyacGyCBnO82ziiYfE7tMi6vznQ7Gu02KnCFaP1TvxiHkBf90iGL+nJ3pEKA7D9Lnwbz9Gg==", "dependencies": { - "@hellocoop/router": "*", + "@hellocoop/router": "^1.9.4", "dotenv": "^16.3.1", "fastify-plugin": "^4.5.1" }, @@ -121,12 +125,12 @@ } }, "node_modules/@hellocoop/router": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@hellocoop/router/-/router-1.8.6.tgz", - "integrity": "sha512-9DyQq0zdelp9i/jNuW9wywDytK/lL5mAFpoX5v2yIKR5vmcfCvA6IZnSfqmY+bQ1VtzjNL5VkWi6vRWhbgKmVQ==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@hellocoop/router/-/router-1.9.4.tgz", + "integrity": "sha512-OWeEASp4So+f8cUsdSAO1mo/1+lV5DxqPqAkBdc38XFQ21/Fw8+iRUwxGmOzFUVD35xPzE1H3rEb//QyAxhZzQ==", "dependencies": { - "@hellocoop/constants": "*", - "@hellocoop/core": "*", + "@hellocoop/constants": "~1.1.5", + "@hellocoop/core": "~1.4.5", "cookie": "^0.5.0" }, "engines": { @@ -252,9 +256,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -278,18 +282,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", - "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", + "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==", "dev": true, "dependencies": { - "playwright": "1.42.1" + "playwright": "1.45.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@tsconfig/node10": { @@ -329,24 +333,24 @@ "dev": true }, "node_modules/@types/jws": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@types/jws/-/jws-3.2.9.tgz", - "integrity": "sha512-xAqC7PI7QSBY3fXV1f2pbcdbBFoR4dF8+lH2z6MfZQVcGe14twYVfjzJ3CHhLS1NHxE+DnjUR5xaHu2/U9GGaQ==", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/@types/jws/-/jws-3.2.10.tgz", + "integrity": "sha512-cOevhttJmssERB88/+XvZXvsq5m9JLKZNUiGfgjUb5lcPRdV2ZQciU6dU76D/qXXFYpSqkP3PrSg4hMTiafTZw==", "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/mocha": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", - "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz", + "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==", "dev": true }, "node_modules/@types/node": { - "version": "20.11.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", - "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -369,9 +373,9 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "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" @@ -381,23 +385,26 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { "node": ">=0.4.0" } }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", + "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", @@ -421,9 +428,9 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" @@ -466,11 +473,6 @@ "node": ">= 8" } }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -503,13 +505,11 @@ } }, "node_modules/avvio": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", - "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.2.tgz", + "integrity": "sha512-st8e519GWHa/azv8S87mcJvZs4WsgTBjOw/Ih1CP6u+8SZvcOeAYNG6JbsIrAUUJJ7JfmrnOkR8ipDS+u9SIRQ==", "dependencies": { "@fastify/error": "^3.3.0", - "archy": "^1.0.0", - "debug": "^4.0.0", "fastq": "^1.17.1" } }, @@ -565,12 +565,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -656,16 +656,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -678,6 +672,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -748,9 +745,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -784,9 +781,9 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, "engines": { "node": ">=0.3.1" @@ -890,19 +887,35 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-json-stringify": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.13.0.tgz", - "integrity": "sha512-XjTDWKHP3GoMQUOfnjYUbqeHeEt+PvYgvBdG2fRSmYaORILbSr8xTJvZX+w1YSAP5pw2NwKrGRmQleYueZEoxw==", + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.1.tgz", + "integrity": "sha512-KAdnLvy1yu/XrRtP+LJnxbBGrhN+xXu+gt3EUvZhYGKCr3lFHq/7UFJHHFgmJKoqlh6B40bZLEv7w46B0mqn1g==", "dependencies": { "@fastify/merge-json-schemas": "^0.1.0", "ajv": "^8.10.0", - "ajv-formats": "^2.1.1", + "ajv-formats": "^3.0.1", "fast-deep-equal": "^3.1.3", "fast-uri": "^2.1.0", "json-schema-ref-resolver": "^1.0.1", "rfdc": "^1.2.0" } }, + "node_modules/fast-json-stringify/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/fast-querystring": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", @@ -920,14 +933,14 @@ } }, "node_modules/fast-uri": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", - "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz", + "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA==" }, "node_modules/fastify": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.26.2.tgz", - "integrity": "sha512-90pjTuPGrfVKtdpLeLzND5nyC4woXZN5VadiNQCicj/iJU4viNHKhsAnb7jmv1vu2IzkLXyBiCzdWuzeXgQ5Ug==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.28.1.tgz", + "integrity": "sha512-kFWUtpNr4i7t5vY2EJPCN2KgMVpuqfU4NjnJNCgiNB900oiDeYqaNDRcAfeBbOF5hGixixxcKnOU4KN9z6QncQ==", "funding": [ { "type": "github", @@ -948,7 +961,7 @@ "fast-json-stringify": "^5.8.0", "find-my-way": "^8.0.0", "light-my-request": "^5.11.0", - "pino": "^8.17.0", + "pino": "^9.0.0", "process-warning": "^3.0.0", "proxy-addr": "^2.0.7", "rfdc": "^1.3.0", @@ -971,9 +984,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -983,13 +996,13 @@ } }, "node_modules/find-my-way": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", - "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.0.tgz", + "integrity": "sha512-HdWXgFYc6b1BJcOBDBwjqWuHJj1WYiqrxSh25qtU4DabpMFdj/gSunNBQb83t+8Zt67D7CXEzJWTkxaShMTMOA==", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", - "safe-regex2": "^2.0.0" + "safe-regex2": "^3.1.0" }, "engines": { "node": ">=14" @@ -1021,9 +1034,9 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", "dev": true, "dependencies": { "cross-spawn": "^7.0.0", @@ -1077,6 +1090,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -1164,6 +1178,7 @@ "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", @@ -1176,9 +1191,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/ioredis": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", - "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", @@ -1285,15 +1300,15 @@ "dev": true }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.2.tgz", + "integrity": "sha512-qH3nOSj8q/8+Eg8LUPOq3C+6HWkpUioIjDsq1+D4zY91oZvpPttw8GwtF1nReRYKXl+1AORyFqtm2f5Q1SB6/Q==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=14" + "node": "14 >=14.21 || 16 >=16.20 || >=18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1357,9 +1372,9 @@ } }, "node_modules/light-my-request": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.12.0.tgz", - "integrity": "sha512-P526OX6E7aeCIfw/9UyJNsAISfcFETghysaWHQAlQYayynShT08MOj4c6fBCvTWBrHXSvqBAKDp3amUPSCQI4w==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.13.0.tgz", + "integrity": "sha512-9IjUN9ZyCS9pTG+KqTDEQo68Sui2lHsYBrfMyVUTTZ3XhH8PMZq7xO94Kr+eP9dhi/kcKsx4N41p2IXEBil1pQ==", "dependencies": { "cookie": "^0.6.0", "process-warning": "^3.0.0", @@ -1408,15 +1423,10 @@ } }, "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true }, "node_modules/make-error": { "version": "1.3.6", @@ -1435,9 +1445,9 @@ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" }, "node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -1447,40 +1457,40 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/mocha": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", - "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.6.0.tgz", + "integrity": "sha512-hxjt4+EEB0SA0ZDygSS015t65lJw/I2yRCS3Ae+SJ5FrbzrXgfYwJr96f0OvIXdj7h4lv/vLCrH3rkiuizFSvw==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -1502,9 +1512,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", - "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", "funding": [ { "type": "github", @@ -1574,6 +1584,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1593,30 +1609,21 @@ } }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1630,68 +1637,68 @@ } }, "node_modules/pino": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.19.0.tgz", - "integrity": "sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.2.0.tgz", + "integrity": "sha512-g3/hpwfujK5a4oVbaefoJxezLzsDgLcNJeITvC6yrfwYeT9la+edCK42j5QpEQSQCZgTKapXvnQIdgZwvRaZug==", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.1.0", - "pino-std-serializers": "^6.0.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^7.0.0", "process-warning": "^3.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.7.0", - "thread-stream": "^2.0.0" + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", - "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "node_modules/pino-std-serializers": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", - "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==" }, "node_modules/playwright": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", - "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", + "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", "dev": true, "dependencies": { - "playwright-core": "1.42.1" + "playwright-core": "1.45.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", - "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", + "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/playwright/node_modules/fsevents": { @@ -1827,11 +1834,11 @@ } }, "node_modules/ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", "engines": { - "node": ">=4" + "node": ">=10" } }, "node_modules/reusify": { @@ -1844,14 +1851,14 @@ } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.9.tgz", + "integrity": "sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA==", "dev": true, "dependencies": { "glob": "^10.3.7" @@ -1860,38 +1867,36 @@ "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=14" + "node": "14 >=14.20 || 16 >=16.20 || >=18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -1923,11 +1928,11 @@ ] }, "node_modules/safe-regex2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", - "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", "dependencies": { - "ret": "~0.2.0" + "ret": "~0.4.0" } }, "node_modules/safe-stable-stringify": { @@ -1949,12 +1954,9 @@ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "bin": { "semver": "bin/semver.js" }, @@ -1963,9 +1965,9 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "dependencies": { "randombytes": "^2.1.0" @@ -2010,9 +2012,9 @@ } }, "node_modules/sonic-boom": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.0.tgz", - "integrity": "sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.0.1.tgz", + "integrity": "sha512-hTSD/6JMLyT4r9zeof6UtuBDpjJ9sO08/nmS5djaA9eozT9oOlNdpXSnzcgj4FTqpk3nkLrs61l4gip9r1HCrQ==", "dependencies": { "atomic-sleep": "^1.0.0" } @@ -2120,9 +2122,9 @@ } }, "node_modules/thread-stream": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", - "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", "dependencies": { "real-require": "^0.2.0" } @@ -2200,9 +2202,9 @@ } }, "node_modules/typescript": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2248,9 +2250,9 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true }, "node_modules/wrap-ansi": { @@ -2303,11 +2305,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -2327,9 +2324,9 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "engines": { "node": ">=10" diff --git a/package.json b/package.json index 430d19c..0949252 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "client-as", - "version": "1.0.14", + "version": "1.1.3", "description": "Hellō Client and Authorization Server", "main": "server.js", "directories": { @@ -11,9 +11,10 @@ }, "scripts": { "start": "node dist/server.js", + "debug": "node --inspect dist/server.js", "build": "rimraf dist && tsc -p tsconfig.prod.json", "release": "npm run build && ./scripts/release.sh", - "playwright": "npm run build && docker-compose -f docker-compose.playwright.yml up --build -d && cd playwright && playwright test && docker-compose -f ../docker-compose.playwright.yml down", + "playwright": "npm run build && docker compose -f docker-compose.playwright.yml up --build -d && cd playwright && playwright test && docker compose -f ../docker-compose.playwright.yml down", "test": "OAUTH_DPOP=true mocha -r ts-node/register tests/*.spec.ts && mocha -r ts-node/register tests/refresh-token.spec.ts" }, "author": "", @@ -34,7 +35,8 @@ }, "dependencies": { "@fastify/formbody": "^7.4.0", - "@hellocoop/fastify": "latest", + "@hellocoop/fastify": "^1.11.5", + "@hellocoop/router": "^1.9.4", "cookie": "^0.6.0", "fastify": "^4.26.2", "ioredis": "^5.3.2", diff --git a/playwright/sync/sync.js b/playwright/sync/sync.js index 809bb3a..0ed6909 100644 --- a/playwright/sync/sync.js +++ b/playwright/sync/sync.js @@ -9,32 +9,32 @@ function isDefaultMockValues() { return JSON.stringify(mockValues) === JSON.stringify(defaultMockValues); } -// GET /sync +// GET /sync -- get what was lasted posted fastify.get('/sync', async (request, reply) => { return lastSyncValue; }); -// DELETE /sync +// DELETE /sync -- clear the last posted value fastify.delete('/sync', async (request, reply) => { lastSyncValue = null; return lastSyncValue; }); -// POST /sync +// POST /sync -- this is the actual call being mocked fastify.post('/sync', async (request, reply) => { - if (isDefaultMockValues()) { - lastSyncValue = request.body + lastSyncValue = request.body + if (isDefaultMockValues()) { return reply.code(200).send({}); } return reply.code(mockValues.code).send(mockValues.response); }); -// GET /mock +// GET /mock -- get the current mock values fastify.get('/mock', async (request, reply) => { return mockValues; }); -// POST /mock +// POST /mock -- set what will be mocked fastify.post('/mock', async (request, reply) => { const { code, response } = request.body; if (!code) { diff --git a/playwright/tests/server.test.ts b/playwright/tests/server.test.ts index 1360d6d..353aaaf 100644 --- a/playwright/tests/server.test.ts +++ b/playwright/tests/server.test.ts @@ -2,11 +2,14 @@ import { AUTH_ROUTE, TOKEN_ENDPOINT, INTROSPECTION_ENDPOINT, + COOKIES_ENDPOINT, } from '../../src/constants' + const ISSUER = 'http://localhost:3000' const CLIENT_API = ISSUER + AUTH_ROUTE const MOCK_API = 'http://localhost:3333/mock' const SYNC_MOCK_API = 'http://localhost:8888/mock' +const SYNC_ENDPOINT = 'http://localhost:8888/sync' import { test, expect } from '@playwright/test'; @@ -48,6 +51,8 @@ test.describe('Testing Client', () => { const response = await page.request.get(CLIENT_API+'?op=auth') const json = await response.json() expect(json).toEqual(loggedOut) + const cookies = await page.context().cookies(); + expect(cookies).toEqual([]) }) test('Logged Out', async ({ page, context }) => { @@ -80,6 +85,9 @@ test.describe('Testing Client', () => { const json = await response.json() delete json.iat expect(json).toEqual(loggedIn) + const cookies = await page.context().cookies(); + expect(cookies).toHaveLength(1) + expect(cookies[0].name).toBe('hellocoop_auth') }) }); @@ -93,7 +101,7 @@ test.describe('Testing Authorization Server', () => { expect(json).toEqual(loggedOut) }) - test('AS login', async ({ page, request, context }) => { + test('AS login', async ({ page, request }) => { const response = await request.post(ISSUER + TOKEN_ENDPOINT, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' @@ -108,6 +116,17 @@ test.describe('Testing Authorization Server', () => { const query = new URLSearchParams({op: 'login', nonce, target_uri: CLIENT_API+'?op=auth'}) await page.goto(CLIENT_API+'?'+query.toString()) const body = await page.textContent('body'); + + + // check cookie tokens that are set + const cookieResponse1 = await request.get(ISSUER + COOKIES_ENDPOINT) + const cookies1 = await cookieResponse1.json() + expect(cookies1).toBeDefined() + expect(cookies1.cookies).toBeDefined() + expect(cookies1.cookies.session_token).toBeDefined() + expect(cookies1.cookies.access_token).toBeUndefined() + expect(cookies1.cookies.refresh_token).toBeUndefined() + try { const json = JSON.parse(body as string); delete json.iat @@ -126,12 +145,37 @@ test.describe('Testing Authorization Server', () => { expect(jsonAS2).toBeDefined() expect(jsonAS2.loggedIn).toBe(true) + // check cookie tokens that are set + const cookieResponse2 = await request.get(ISSUER + COOKIES_ENDPOINT) + const cookies2 = await cookieResponse2.json() + expect(cookies2).toBeDefined() + expect(cookies2.cookies).toBeDefined() + expect(cookies2.cookies.session_token).toBeUndefined() + expect(cookies2.cookies.access_token).toBeDefined() + expect(cookies2.cookies.refresh_token).toBeDefined() + const response3 = await request.get(ISSUER + INTROSPECTION_ENDPOINT) const jsonAS3 = await response3.json() expect(jsonAS3).toBeDefined() const { sub, iss } = jsonAS3 expect(sub).toEqual(loggedIn.sub) expect(iss).toEqual(ISSUER) + + + const logoutResponse = await request.get(CLIENT_API+'?op=logout') + expect(logoutResponse.status()).toBe(200) + const jsonLogout = await logoutResponse.json() + expect(jsonLogout).toEqual(loggedOut) + + // check we have no cookies after logging out + const cookieResponse3 = await request.get(ISSUER + COOKIES_ENDPOINT) + expect(cookieResponse3.status()).toBe(200) + const cookies3 = await cookieResponse3.json() + expect(cookies3).toBeDefined() + expect(cookies3.cookies).toBeDefined() + expect(cookies3.cookies.session_token).toBeUndefined() + expect(cookies3.cookies.access_token).toBeUndefined() + expect(cookies3.cookies.refresh_token).toBeUndefined() }) }); @@ -254,7 +298,9 @@ test.describe('Testing Authorization Server Errors', () => { }, data: JSON.stringify({ code: 200, - response: '{"accessDenied":true}' + response: { + accessDenied:true + } }) }) @@ -265,12 +311,6 @@ test.describe('Testing Authorization Server Errors', () => { const finalUrl = page.url(); const urlParams = new URLSearchParams(new URL(finalUrl).search); - - // urlParams.forEach((value, key) => { - // console.log(`${key}: ${value}`); - // }); - - expect(urlParams.get('error')).toBe('access_denied') expect(urlParams.get('op')).toBe('auth') const body = await page.textContent('body'); @@ -390,4 +430,96 @@ test.describe('Testing Authorization Server Errors', () => { expect(iss).toEqual(ISSUER) }) + test('User DB Sync gets origin data returns payload sub and scope', async ({ page, request, context }) => { + // set mock response + const syncMockResponse = await request.post(SYNC_MOCK_API, { + headers: { + 'Content-Type': 'application/json' + }, + data: JSON.stringify({ + code: 200, + response: { + payload: { + sub: 'app-sub-id', + scope: 'test', + client_id: 'test-app', + } + } + }) + + }) + expect(syncMockResponse.status()).toBe(200) + + // start login flow by trying to get an access token, passing in the client_id and redirect_uri which will be + // passed to the sync endpoint as an origin object + const tokenRequest = { + grant_type: 'cookie_token', + client_id: 'test-app', + } + const data = new URLSearchParams(tokenRequest).toString() + const response = await request.post(ISSUER + TOKEN_ENDPOINT, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data + }) + const jsonAS = await response.json() + expect(jsonAS).toBeDefined() + expect(jsonAS.loggedIn).toBe(false) + const nonce = jsonAS.nonce + expect(nonce).toBeDefined() + + // start Hello login flow -- this should cause the sync endpoint to be called with the origin data + const query = new URLSearchParams({op: 'login', nonce, target_uri: CLIENT_API+'?op=auth'}) + await page.goto(CLIENT_API+'?'+query.toString()) + const body = await page.textContent('body'); + try { + const json = JSON.parse(body as string); + delete json.iat + expect(json).toEqual(loggedIn) + } + catch (e) { + expect(e).toBeNull() + } + + // check what the sync endpoint received + const syncRequest = await request.get(SYNC_ENDPOINT) + expect(syncRequest.status()).toBe(200) + const syncRequestJson = await syncRequest.json() + const { payload, token, origin } = syncRequestJson + expect(payload).toBeDefined() + expect(token).toBeDefined() + expect(origin).toBeDefined() + expect(payload.sub).toEqual(loggedIn.sub) + expect(payload.aud).toEqual('hello-docker-test-client') + expect(payload.name).toEqual(loggedIn.name) + expect(payload.email).toEqual(loggedIn.email) + expect(payload.email_verified).toEqual(loggedIn.email_verified) + expect(origin.client_id).toEqual('test-app') + expect(origin.target_uri).toEqual(CLIENT_API+'?op=auth') + + // get the access token again + const response2 = await request.post(ISSUER + TOKEN_ENDPOINT, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + data, + }) + const jsonAS2 = await response2.json() + expect(jsonAS2).toBeDefined() + expect(jsonAS2.loggedIn).toBe(true) + + // get access token contents from the introspection endpoint using cookie_token + const response3 = await request.get(ISSUER + INTROSPECTION_ENDPOINT) + const jsonAS3 = await response3.json() + + expect(jsonAS3).toBeDefined() + const { active, sub, iss, hello_sub, scope, client_id } = jsonAS3 + expect(active).toBe(true) + expect(sub).toEqual('app-sub-id') + expect(iss).toEqual(ISSUER) + expect(hello_sub).toEqual(loggedIn.sub) + expect(scope).toEqual('test') + expect(client_id).toEqual('test-app') + }) }); diff --git a/sequence/API-call.puml b/sequence/API-call.puml new file mode 100644 index 0000000..25ea03f --- /dev/null +++ b/sequence/API-call.puml @@ -0,0 +1,32 @@ +@startuml + +title API Calls +participant SDK as sdk +participant Webview as wv +participant "API\nServer" as api +participant "Authorization\nServer" as as + +activate wv + wv -> api: (access cookie) + activate api + api -> api: get access token from cookie + api -> as: GET /api/auth/v1/jwks\n(cache for future) + activate as + as --> api: JWKs Data + deactivate as + api -> api: verify access token + api --> wv: API response + deactivate api +deactivate wv +break +activate sdk + sdk -> api: Authorization: Bearer + api -> as: GET /api/auth/v1/jwks\n(cache for future) + activate as + as --> api: JWKs Data + deactivate as + api -> api: verify access token + api --> sdk: API response +deactivate + +@enduml \ No newline at end of file diff --git a/sequence/login-flow-sdk.puml b/sequence/login-flow-sdk.puml new file mode 100644 index 0000000..ae858c5 --- /dev/null +++ b/sequence/login-flow-sdk.puml @@ -0,0 +1,83 @@ +@startuml +title Hellō Login Flow (SDK) +actor User as user +participant SDK as sdk +participant Webview as wv +participant "Safe Browsing\nWindow" as sbw +participant "Hellō\nClient & AS" as hc +participant "Web\nServer" as ws +participant "User\nDB Service" as db +participant "Redis\nCache" as redis +participant "Hellō\nWallet" as hello + +user -> sdk: Clicks Widget +activate sdk + sdk -> wv: Open Webview + activate wv + wv -> ws: Fetch Homepage + ws --> wv: Homepage + wv -> hc: (A)\nPOST /token\ngrant_type=cookie_token&\nclient_id=webview_version + activate hc + hc -> hc: (1)\nno cookies + hc -> redis: create new session, nonce + redis --> hc: + hc --> wv: loggedIn=false\nnonce + deactivate hc + wv --> user: Homepage, User Logged Out + user -> wv: Clicks Login + wv -> sdk: Start Login Flow\nnonce + deactivate wv + sdk -> sbw: (B)\nOpen Login Page\n\n/api/auth/v1?\nop=login&\nnonce=&\nscope=&\nprovider_hint=&\ntarget_uri= + deactivate sdk + activate sbw + sbw -> hc: open login page + activate hc + hc --> sbw: Hellō Login Redirect + sbw -> hello: Fetch Login Page + activate hello + hello --> sbw: Hello Login Page + sbw --> user: Hello Login Page + user -> sbw: Authenticate + sbw -> hello: Authenticate + hello --> sbw: Authenticated\nRedirect to Hellō Client + sbw -> hc: Auth Code from Hellō + hc -> hello: Fetch ID Token with Auth Code + hello --> hc: ID Token + deactivate + hc -> redis: (2)\nLookup nonce + redis --> hc: nonce data + hc -> db: loginSync\n id_token, payload\n origin {client_id,target_uri} + activate db + db -> db: (C)\nCreate or\nupdate user\nprocess access + db --> hc: target_uri, client_id, scope + deactivate db + hc -> redis: Update nonce + redis --> hc: + hc --> sbw: Auth success + sbw --> sdk: Auth success + deactivate sbw + activate sdk + sdk -> hc: (D)\nPOST /api/auth/v1/token\ngrant_type=authorization_code&\ncode=nonce&\nclient_id=sdk_version + hc -> redis: (3)\nLookup / upate nonce + redis --> hc: nonce data + hc -> hc: Future: check if\nuser logged out + hc --> sdk: Access Token, Refresh Token + sdk -> sdk: Store Tokens + sdk --> wv: Open Webview + activate wv + wv -> ws: Fetch Homepage + ws --> wv: Homepage + wv -> hc: (E)\nPOST /api/auth/v1/token\ngrant_type=cookie_token&\nclient_id=webview_version + hc --> redis: (4)\nLookup nonce + redis --> hc: nonce data + hc -> hc: Future: check if\nuser logged out + hc --> wv: set access and refresh cookies\nloggedIn=true + wv --> user: Home Page, User Logged In + deactivate ws + deactivate wv + sdk -> hc: (F)\nPOST /api/auth/v1/token\ngrant_type=refresh_token&\nclient_id=sdk_version&\nrefresh_token=refresh_token + hc -> hc: (5)\nFuture: check if\nuser logged out + hc --> sdk: Access Token, Refresh Token +deactivate + +@enduml \ No newline at end of file diff --git a/sequence/logout.puml b/sequence/logout.puml new file mode 100644 index 0000000..6a83450 --- /dev/null +++ b/sequence/logout.puml @@ -0,0 +1,36 @@ +@startuml +title User Logout +actor User as user +participant SDK as sdk +participant Webview as wv +participant "Authorization\nServer" as as +participant "Web\nServer" as ws +participant "User\nDatabase" as db +actor Admin as admin + +admin -> db: logout = now +user -> sdk: Clicks Widget +activate sdk + sdk -> wv: Open Webview + activate wv + wv -> ws: Fetch Homepage + ws --> wv: Homepage + wv -> as: POST /token\ngrant_type=cookie\n&client_id=webview_version + activate as + as -> db: Find User + db --> as: User Data\nlogout later than token + as --> wv: {loggedIn=false,nonce=} + deactivate as + wv --> user: Homepage, User Logged Out + deactivate wv +deactivate sdk +break + sdk -> as: client_id=sdk_version\n&refresh_token=refresh_token + activate sdk + activate as + as -> db: Find User + db --> as: User Data\nlogout later than token + as --> sdk: {error=access_denied} + deactivate as +deactivate sdk +@enduml \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index dcd07a7..048819a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -6,7 +6,7 @@ import jws, { Algorithm, Header } from 'jws' import jwkToPem, { JWK } from 'jwk-to-pem' import { randomUUID, createHash } from 'crypto'; import { serialize as serializeCookie, parse as parseCookies } from 'cookie' -import { helloAuth, HelloConfig, LoginSyncParams, LoginSyncResponse } from '@hellocoop/fastify' +import { helloAuth, HelloConfig, LoginSyncParams, LoginSyncResponse, LogoutSyncParams, LogoutSyncResponse } from '@hellocoop/fastify' import { PUBLIC_JWKS, PRIVATE_KEY, PUBLIC_KEY } from './jwks' import * as state from './state' @@ -24,20 +24,24 @@ import { DPOP_LIFETIME, LOGOUT_ENDPOINT } from './constants' +import { log } from 'console'; +import { create } from 'domain'; +import { stat } from 'fs'; interface Payload { iss: string sub: string aud: string - client_id: string token_type: string iat: number exp: number jti: string cnf?: { jkt: string - } - + }, + client_id?: string + hello_sub?: string + scope?: string } const HOST = process.env.HOST @@ -124,7 +128,7 @@ const generateThumbprint = function(jwk: JWK) { return hash; } -const setTokenCookies = (reply: FastifyReply, access_token: string, refresh_token: string) => { +const createTokenCookies = ( access_token: string, refresh_token: string) => { const accessTokenCookie = serializeCookie('access_token', access_token || '', { maxAge: access_token ? ACCESS_LIFETIME : 0, @@ -151,7 +155,7 @@ const setTokenCookies = (reply: FastifyReply, access_token: string, refresh_toke sameSite: SAME_SITE, }) - reply.header('Set-Cookie', [accessTokenCookie, refreshTokenCookie, sessionTokenCookie]); + return [accessTokenCookie, refreshTokenCookie, sessionTokenCookie] } const setSessionCookie = (reply: FastifyReply, session_token: string) => { @@ -184,7 +188,7 @@ const validateDPoP = (req: FastifyRequest): string => { if (Array.isArray(dpop)) { throw new TokenError(400, 'Only one DPoP header is allowed') } - const { header, payload } = jws.decode(dpop as string, { json: true }) + const { header, payload } = jws.decode(dpop as string, { json: true }) || {} as jws.Signature if (!header || !payload) { throw new TokenError(400, 'DPoP header is invalid') } @@ -240,13 +244,18 @@ const refreshFromCode = async (code: string, client_id: string, jkt: string): Pr // future - logout user to revoke issued refresh_token throw new TokenError(400, 'code has already been used') } + if (currentState.client_id && currentState.client_id !== client_id) { + throw new TokenError(400, 'code client_id does not match') + } currentState.code_used = now await state.update(code, currentState) const payload: Payload = { iss: ISSUER, - sub: currentState.sub as string, - aud: currentState.aud as string, + sub: currentState.sub, + aud: currentState.aud, + hello_sub: currentState.hello_sub, + scope: currentState.scope, client_id, token_type: 'refresh_token', iat: now, @@ -271,7 +280,7 @@ const refreshFromCode = async (code: string, client_id: string, jkt: string): Pr } const refreshFromRefresh = (refresh_token: string): string => { - const { header, payload } = jws.decode(refresh_token, { json: true }) + const { header, payload } = jws.decode(refresh_token, { json: true }) || {} as jws.Signature if (!header || !payload) { throw new TokenError(400, 'refresh_token is invalid') } @@ -297,7 +306,7 @@ const refreshFromRefresh = (refresh_token: string): string => { const refreshFromSession = async (session_token: string) => { // lookup session_token and get payload - const { header, payload } = jws.decode(session_token, { json: true }) + const { header, payload } = jws.decode(session_token, { json: true }) || {} as jws.Signature // TODO -- verify session_token if (!header || !payload) { throw new TokenError(400, 'session_token is invalid') @@ -320,11 +329,13 @@ const refreshFromSession = async (session_token: string) => { if (currentState.iss !== ISSUER) { throw new TokenError(400, 'session_token invalid issuer') } - const refreshPayload = { + const refreshPayload: Payload = { iss: ISSUER, - sub: currentState.sub, - aud: currentState.aud, - client_id: payload.client_id, + sub: currentState.sub as string, + aud: currentState.aud as string, + hello_sub: currentState.hello_sub, + scope: currentState.scope, + client_id: currentState.client_id, token_type: 'refresh_token', iat: now, exp: now + REFRESH_LIFETIME, @@ -339,7 +350,7 @@ const refreshFromSession = async (session_token: string) => { } const accessFromRefresh = (refresh_token: string): string => { - const { header, payload } = jws.decode(refresh_token, { json: true }) + const { header, payload } = jws.decode(refresh_token, { json: true }) || {} as jws.Signature if (!header || !payload) { throw new TokenError(400, 'refresh_token is invalid') } @@ -365,14 +376,15 @@ const accessFromRefresh = (refresh_token: string): string => { return newAccessToken } -const makeSessionToken = async (client_id: string): Promise<{session_token: string, nonce: string}> => { +const makeSessionToken = async (origin: state.Origin): Promise<{session_token: string, nonce: string}> => { const nonce = randomUUID() const now = Math.floor(Date.now() / 1000) const currentState: state.State = { - iss: ISSUER, loggedIn: false, + iss: ISSUER, exp: now + STATE_LIFETIME, - nonce + nonce, + origin, } await state.create(nonce, currentState) const params = { @@ -382,8 +394,7 @@ const makeSessionToken = async (client_id: string): Promise<{session_token: stri iss: ISSUER, iat: now, exp: now + STATE_LIFETIME, - client_id, - nonce + nonce, }, privateKey: PRIVATE_KEY } @@ -394,7 +405,7 @@ const makeSessionToken = async (client_id: string): Promise<{session_token: stri const tokenEndpoint = async (req: FastifyRequest, reply: FastifyReply) => { const { grant_type, client_id, refresh_token, code } = req.body as - { grant_type: string, client_id: string, refresh_token: string, code: string } + { grant_type: string, client_id: string, refresh_token: string, code: string} // console.log({grant_type, headers: req.headers, cookies: req.headers['cookie']}) @@ -423,7 +434,8 @@ const tokenEndpoint = async (req: FastifyRequest, reply: FastifyReply) => { return reply.code(400).send({error:'invalid_request', error_description:'refresh_token is required'}) } const jwt = validateDPoP(req) - const {payload} = jws.decode(refresh_token, { json: true }) + + const { payload } = jws.decode(refresh_token, { json: true }) || {} as jws.Signature if (USE_DPOP) { if (!payload?.cnf?.jkt) { throw new TokenError(400, 'refresh_token is invalid') @@ -444,14 +456,15 @@ const tokenEndpoint = async (req: FastifyRequest, reply: FastifyReply) => { }) } - if (grant_type === 'cookie_token') { // non-standard + if (grant_type === 'cookie_token') { // non standard grant_type if (!client_id) { return reply.code(400).send({error:'invalid_request', error_description:'client_id is required'}) } const { session_token, refresh_token } = getCookies(req) if (!session_token && !refresh_token) { // no existing session - const { session_token, nonce } = await makeSessionToken(client_id) + const origin = { client_id } + const { session_token, nonce } = await makeSessionToken( origin ) if (!session_token) { return reply.code(500).send({error:'server_error: session_token not created'}) } @@ -470,7 +483,7 @@ const tokenEndpoint = async (req: FastifyRequest, reply: FastifyReply) => { ? refreshFromRefresh(refresh_token) : await refreshFromSession(session_token) const newAccessToken = accessFromRefresh(newRefreshToken) - setTokenCookies(reply, newAccessToken, newRefreshToken) + reply.header('Set-Cookie', createTokenCookies( newAccessToken, newRefreshToken )) return reply.send({ loggedIn: true }) @@ -513,7 +526,7 @@ const introspectEndpoint = async (req: FastifyRequest, reply: FastifyReply) => { } if (!jws.verify(token, 'RS256', PUBLIC_KEY)) return reply.send({active: false}) - const { payload } = jws.decode(token, { json: true }) + const { payload } = jws.decode(token, { json: true }) || {} as jws.Signature if (!payload) { return reply.send({active: false}) } @@ -534,26 +547,90 @@ if (loginSyncUrl) { } const logoutUser = async (nonce: string) => { - const result = await state.update(nonce, { - loggedIn: false, - }) + const result = await state.remove(nonce) +} + +const logoutSync = async (params: LogoutSyncParams): Promise => { + + try { + const clearedCookies = createTokenCookies('', '') + + debugger + + console.log('logoutSync called x', clearedCookies) + + console.log('logoutSync get', params.cbRes) + // console.log('logoutSync get', params.cbRes.getHeaders()) + // + params.cbRes.setHeader('Set-Cookie', clearedCookies) + + console.log('logoutSync postheaders', params.cbRes.getHeaders()) + + } catch (e) { + console.error('logoutSync error', e) + } + + + return null } const loginSync = async ( params: LoginSyncParams ): Promise => { - const { payload, token } = params - const { nonce, sub } = payload + const { payload, token, target_uri } = params + const { nonce, sub, aud } = payload - if (!PRODUCTION) { - console.log('loginSync', {payload, token}) +// TBD - move reading in state further up so that we can include state in call to loginSyncUrl + + const now = Math.floor(Date.now() / 1000) + + const currentState = await state.read(nonce) + if (!currentState) { + console.error({error:'invalid_request', error_description:'nonce is invalid'}) + return {} + } + if (currentState.loggedIn) { + console.error({error:'invalid_request', error_description:'nonce is already logged in'}) + return {} + } + if ((currentState.exp ?? 0) < now) { + console.error({error:'invalid_request', error_description:'state has expired'}) + return {} } + // we have a valid state to change to sync login across channels + // this is what we hope have in the access_token + const statePayload: state.State = { + loggedIn: true, + iss: ISSUER, + exp: now + STATE_LIFETIME, + nonce, + sub, + aud, + } + + if (currentState?.origin?.client_id) { + statePayload.client_id = currentState.origin.client_id + } + + // POTENTIAL FUTURE - reduce size of hello_auth cookie + // this is what is returned from op=auth + // cookie will contain updatedAuth + defaults of `isLoggedIn`, `sub`, and `iat` + // const hello_auth: { updatedAuth: { app_sub?: string } } = { updatedAuth: {} } + + const syncResponse: LoginSyncResponse = {} if (loginSyncUrl) { // see if user is allowed to login const response = await fetch(loginSyncUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ payload, token }) + body: JSON.stringify({ + payload, + token, + origin: { + client_id: currentState?.origin?.client_id, + target_uri + } + }) }) if (!response.ok) { console.log(`loginSyncUrl ${loginSyncUrl} returned ${response.status} - access denied for sub ${sub}`) @@ -561,14 +638,30 @@ const loginSync = async ( params: LoginSyncParams ): Promise return { accessDenied: true } } // we have a 2xx response - if (response.status === 200) { // we have content + if ((response.status === 200) && (response.headers.get('content-type')?.includes('application/json'))) { try { const json = await response.json() - if (json?.accessDenied) { + const { accessDenied, payload, target_uri: new_target_uri } = json + if (accessDenied) { console.log('loginSync - access denied for sub', sub) await logoutUser(nonce) return { accessDenied: true} } + if (payload) { + if (payload.sub) { + statePayload.sub = payload.sub + statePayload.hello_sub = sub + // hello_auth.updatedAuth.app_sub = payload.sub // so op=auth has access to the app sub + } + if (payload.scope) + statePayload.scope = payload.scope + if (payload.client_id) + statePayload.client_id = payload.client_id + } + if (new_target_uri) { + // redirect to new_target_uri + syncResponse.target_uri = new_target_uri + } } catch (e) { console.error('loginSync - JSON parsing error', e) } @@ -577,38 +670,9 @@ const loginSync = async ( params: LoginSyncParams ): Promise // fall through to update state as access is granted } - const now = Math.floor(Date.now() / 1000) - const currentState = await state.read(nonce) - if (!currentState) { - console.error({error:'invalid_request', error_description:'nonce is invalid'}) - return {} - } - - if (!PRODUCTION) { - console.log('loginSync', {currentState}) - } + await state.update(nonce, statePayload) - - if (currentState.loggedIn) { - console.error({error:'invalid_request', error_description:'nonce is already logged in'}) - return {} - } - if ((currentState.exp ?? 0) < now) { - console.error({error:'invalid_request', error_description:'state has expired'}) - return {} - } - - // we have a valid state to change to sync login across channels - await state.update(nonce, { - iss: ISSUER, - exp: now + STATE_LIFETIME, - nonce, - loggedIn: true, - sub - }) - - - return {} + return syncResponse // we could minimize hello_auth cookie here by returning an object with updatedAuth } @@ -618,6 +682,7 @@ const helloConfig: HelloConfig = { logConfig: true, apiRoute: AUTH_ROUTE, loginSync, + logoutSync, } // console.log('api.js', {helloConfig}) @@ -642,9 +707,15 @@ const api = (app: FastifyInstance) => { app.get(JWKS_ENDPOINT, (req, reply) => { return reply.send(PUBLIC_JWKS) }) - app.get(AUTH_ROUTE+"/version", (request, reply) => { + app.get(AUTH_ROUTE+"/version", (req, reply) => { return reply.send({version}); }); + if (!PRODUCTION) { + // used to test if cookies are getting cleared + app.get(TOKEN_ENDPOINT+"/cookies", (req, reply) => { + return reply.send({cookies: getCookies(req)}); + }); + } } export { api, PORT, loginSync } // loginSync is exported for testing diff --git a/src/constants.ts b/src/constants.ts index c61d9cd..70f4820 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,6 +7,7 @@ export const LOGOUT_ENDPOINT = AUTH_ROUTE + '/logout' export const REVOCATION_ENDPOINT = AUTH_ROUTE + '/revoke' export const JWKS_ENDPOINT = AUTH_ROUTE + '/jwks' export const INTROSPECTION_ENDPOINT = AUTH_ROUTE + '/introspect' +export const COOKIES_ENDPOINT = TOKEN_ENDPOINT + '/cookies' export const ACCESS_LIFETIME = 5 * 60 // 5 minutes export const STATE_LIFETIME = 5 * 60 // 5 minutes diff --git a/src/state.ts b/src/state.ts index da774e3..97551bb 100644 --- a/src/state.ts +++ b/src/state.ts @@ -32,16 +32,36 @@ if (process.env.REDIS_HOST) { } } -type State = { - loggedIn: boolean, - nonce?: string, - exp?: number, - iss?: string, - aud?: string, - sub?: string, - code_used?: number +type BaseState = { + iss: string, + exp: number, + nonce: string, } +type Origin = { + client_id: string, + target_uri?: string, +} + +type LoggedOutState = { + loggedIn: false, + origin?: Origin, +} + +type LoggedInState = { + loggedIn: true, + aud: string, + sub: string, + code_used?: number, + hello_sub?: string, + scope?: string, + client_id?: string, +} + + +type State = BaseState & (LoggedOutState | LoggedInState); + +// for development! ... in production, use redis const state: Record = {}; const read = async (key: string): Promise => { @@ -55,7 +75,7 @@ const read = async (key: string): Promise => { const create = async (key: string, value: State): Promise => { if (redis) { - await redis.set(key, JSON.stringify(value)); + await redis.set(key, JSON.stringify(value), 'EX', STATE_LIFETIME); } else { state[key] = value; } @@ -69,4 +89,12 @@ const update = async (key: string, value: State): Promise => { } } -export { create, read, update, State }; \ No newline at end of file +const remove = async (key: string): Promise => { + if (redis) { + await redis.del(key); + } else { + delete state[key]; + } +} + +export { create, read, update, remove, State, Origin }; \ No newline at end of file diff --git a/tests/cookie-token.spec.ts b/tests/cookie-token.spec.ts index 06c9c11..bc596fb 100644 --- a/tests/cookie-token.spec.ts +++ b/tests/cookie-token.spec.ts @@ -80,14 +80,13 @@ describe('Cookie Token', () => { assert.strictEqual(sessionCookie.path, TOKEN_ENDPOINT, `session_token cookie path is ${TOKEN_ENDPOINT}`); session_token = sessionCookie.value; assert(session_token, 'session_token cookie value does not exist'); - const { header, payload } = jws.decode(session_token, { json: true }); + const { header, payload } = jws.decode(session_token, { json: true }) as jws.Signature + assert(header, 'session_token cookie value is not a valid JWT'); assert.strictEqual(header.alg, 'RS256', 'session_token alg is not RS256'); const valid = jws.verify(sessionCookie.value, 'RS256', PUBLIC_KEY); assert(valid, 'session_token cookie is not valid'); - assert.strictEqual(payload.iss, "http://localhost:3000", 'session_token iss is not http://localhost:3000'); - assert.strictEqual(payload.client_id, WEBVIEW_CLIENT_ID, `session_token aud is not ${WEBVIEW_CLIENT_ID}`); assert.strictEqual(payload.nonce, nonce, 'session_token nonce does not match returned nonce'); assert.strictEqual(payload.exp - payload.iat, STATE_LIFETIME, `session_token exp - iat is not ${STATE_LIFETIME}`); assert.strictEqual(payload.token_type, 'session_token', 'session_token token_type is not session_token'); @@ -133,7 +132,7 @@ describe('Cookie Token', () => { assert(refreshCookie.maxAge, 'refresh_token cookie does not have maxAge'); assert.strictEqual(refreshCookie.maxAge, REFRESH_LIFETIME, `refresh_token cookie maxAge is not ${REFRESH_LIFETIME}`); assert.strictEqual(refreshCookie.path, TOKEN_ENDPOINT, `refresh_token cookie path is not ${TOKEN_ENDPOINT}`); - const { header: accessHeader, payload: accessPayload } = jws.decode(accessCookie.value, { json: true }); + const { header: accessHeader, payload: accessPayload } = jws.decode(accessCookie.value, { json: true }) as jws.Signature assert(accessHeader, 'access_token cookie value is not a valid JWT'); assert.strictEqual(accessHeader.alg, 'RS256', 'access_token alg is not RS256'); assert.strictEqual(accessHeader.typ, 'at+jwt', 'access_token typ is not at+jwt'); @@ -143,7 +142,7 @@ describe('Cookie Token', () => { assert.strictEqual(accessPayload.client_id, WEBVIEW_CLIENT_ID, `access_token aud is not ${WEBVIEW_CLIENT_ID}`); assert.strictEqual(accessPayload.exp - accessPayload.iat, ACCESS_LIFETIME, `access_token exp - iat is not ${ACCESS_LIFETIME}`); assert.strictEqual(accessPayload.token_type, 'access_token', 'access_token token_type is not access_token'); - const { header: refreshHeader, payload: refreshPayload } = jws.decode(refreshCookie.value, { json: true }); + const { header: refreshHeader, payload: refreshPayload } = jws.decode(refreshCookie.value, { json: true }) as jws.Signature assert(refreshHeader, 'refresh_token cookie value is not a valid JWT'); assert.strictEqual(refreshHeader.alg, 'RS256', 'refresh_token alg is not RS256'); const refreshValid = jws.verify(refreshCookie.value, 'RS256', PUBLIC_KEY); @@ -196,7 +195,7 @@ describe('Cookie Token', () => { assert.strictEqual(refreshCookie.maxAge, REFRESH_LIFETIME, `refresh_token cookie maxAge is not ${REFRESH_LIFETIME}`); assert.strictEqual(refreshCookie.path, TOKEN_ENDPOINT, `refresh_token cookie path is not ${TOKEN_ENDPOINT}`); - const { header: accessHeader, payload: accessPayload } = jws.decode(accessCookie.value, { json: true }); + const { header: accessHeader, payload: accessPayload } = jws.decode(accessCookie.value, { json: true }) as jws.Signature assert(accessHeader, 'access_token cookie value is not a valid JWT'); assert.strictEqual(accessHeader.alg, 'RS256', 'access_token alg is not RS256'); assert.strictEqual(accessHeader.typ, 'at+jwt', 'access_token typ is not at+jwt'); @@ -207,7 +206,7 @@ describe('Cookie Token', () => { assert.strictEqual(accessPayload.exp - accessPayload.iat, ACCESS_LIFETIME, `access_token exp - iat is not ${ACCESS_LIFETIME}`); assert.strictEqual(accessPayload.token_type, 'access_token', 'access_token token_type is not access_token'); - const { header: refreshHeader, payload: refreshPayload } = jws.decode(refreshCookie.value, { json: true }); + const { header: refreshHeader, payload: refreshPayload } = jws.decode(refreshCookie.value, { json: true }) as jws.Signature assert(refreshHeader, 'refresh_token cookie value is not a valid JWT'); assert.strictEqual(refreshHeader.alg, 'RS256', 'refresh_token alg is not RS256'); const refreshValid = jws.verify(refreshCookie.value, 'RS256', PUBLIC_KEY); diff --git a/tests/refresh-token.spec.ts b/tests/refresh-token.spec.ts index e74a439..1083d6f 100644 --- a/tests/refresh-token.spec.ts +++ b/tests/refresh-token.spec.ts @@ -180,7 +180,7 @@ describe('Refresh Token', () => { } else { assert.strictEqual(json.token_type, 'Bearer', 'token_type is not Bearer'); } - const { header: accessHeader, payload: accessPayload } = jws.decode(access_token, { json: true }); + const { header: accessHeader, payload: accessPayload } = jws.decode(access_token, { json: true }) as jws.Signature assert(accessHeader, 'access_token cookie value is not a valid JWT'); assert.strictEqual(accessHeader.alg, 'RS256', 'access_token alg is not RS256'); assert.strictEqual(accessHeader.typ, 'at+jwt', 'access_token typ is not at+jwt'); @@ -195,7 +195,7 @@ describe('Refresh Token', () => { assert.strictEqual(accessPayload.client_id, SDK_CLIENT_ID, `access_token aud is not ${SDK_CLIENT_ID}`); assert.strictEqual(accessPayload.exp - accessPayload.iat, ACCESS_LIFETIME, `access_token exp - iat is not ${ACCESS_LIFETIME}`); assert.strictEqual(accessPayload.token_type, 'access_token', 'access_token token_type is not access_token'); - const { header: refreshHeader, payload: refreshPayload } = jws.decode(refresh_token, { json: true }); + const { header: refreshHeader, payload: refreshPayload } = jws.decode(refresh_token, { json: true }) as jws.Signature assert(refreshHeader, 'refresh_token cookie value is not a valid JWT'); assert.strictEqual(refreshHeader.alg, 'RS256', 'refresh_token alg is not RS256'); assert.strictEqual(refreshHeader.kid, kid, 'refresh_token kid is not the same as the jwks kid'); @@ -231,7 +231,7 @@ describe('Refresh Token', () => { assert(newAccessToken, 'access_token does not exist'); assert(newRefreshToken, 'refresh_token does not exist'); assert.strictEqual(json.expires_in, ACCESS_LIFETIME, `expires_in is not ${ACCESS_LIFETIME}`); - const { header: accessHeader, payload: accessPayload } = jws.decode(newAccessToken, { json: true }); + const { header: accessHeader, payload: accessPayload } = jws.decode(newAccessToken, { json: true }) as jws.Signature assert(accessHeader, 'access_token cookie value is not a valid JWT'); assert.strictEqual(accessHeader.alg, 'RS256', 'access_token alg is not RS256'); assert.strictEqual(accessHeader.typ, 'at+jwt', 'access_token typ is not at+jwt'); @@ -246,7 +246,7 @@ describe('Refresh Token', () => { assert.strictEqual(accessPayload.client_id, SDK_CLIENT_ID, `access_token aud is not ${SDK_CLIENT_ID}`); assert.strictEqual(accessPayload.exp - accessPayload.iat, ACCESS_LIFETIME, `access_token exp - iat is not ${ACCESS_LIFETIME}`); assert.strictEqual(accessPayload.token_type, 'access_token', 'access_token token_type is not access_token'); - const { header: refreshHeader, payload: refreshPayload } = jws.decode(newRefreshToken, { json: true }); + const { header: refreshHeader, payload: refreshPayload } = jws.decode(newRefreshToken, { json: true }) as jws.Signature assert(refreshHeader, 'refresh_token cookie value is not a valid JWT'); assert.strictEqual(refreshHeader.alg, 'RS256', 'refresh_token alg is not RS256'); assert.strictEqual(refreshHeader.kid, kid, 'refresh_token kid is not the same as the jwks kid');