From 53f0e675784ac79d12cf6ea669aba3dad5215ee0 Mon Sep 17 00:00:00 2001 From: Stefan Nienhuis Date: Tue, 7 Jun 2022 18:07:20 +0200 Subject: [PATCH] Fix token refreshing with new authentication flow --- README.md | 19 +- backend/package-lock.json | 278 +++++++----------- backend/package.json | 5 +- backend/serverless.yaml | 14 + backend/src/common/respones.ts | 14 +- backend/src/oauth-begin.ts | 4 +- backend/src/oauth-callback.ts | 4 +- backend/src/oauth-refresh.ts | 57 ++++ backend/src/websocket-manager/routes.ts | 6 +- plugin/config.example.json | 3 +- plugin/config.schema.json | 11 + .../homebridge-ui/src/components/App/App.tsx | 10 +- .../src/components/LegacyAuth/LegacyAuth.tsx | 6 +- plugin/homebridge-ui/src/types/config.ts | 5 + plugin/homebridge-ui/src/types/index.ts | 3 +- plugin/package-lock.json | 4 +- plugin/package.json | 2 +- plugin/src/bold.ts | 49 ++- plugin/src/const.ts | 6 +- plugin/src/index.ts | 2 +- plugin/src/types/config.ts | 2 + 21 files changed, 293 insertions(+), 211 deletions(-) create mode 100644 backend/src/oauth-refresh.ts create mode 100644 plugin/homebridge-ui/src/types/config.ts diff --git a/README.md b/README.md index c160cd4..b49aaff 100644 --- a/README.md +++ b/README.md @@ -32,17 +32,22 @@ For HOOBS or Homebridge without a configuration UI, you can use the [authenticat ### Manual configuration An example configuration can be found in the [config.example.json](config.example.json) file. -| Property | Type | Details | -| -------------- | -------- | ------------------------------------------------ | -| `platform` | `string` | **Required**
Must always be `Bold`. | -| `accessToken` | `string` | **Required**
Access token for the Bold API. | -| `refreshToken` | `string` | **Required**
Refresh token for the Bold API. | +| Property | Type | Details | +| ---------------------- | --------- | ------------------------------------------------------------------------------------------------------------------- | +| `platform` | `string` | **Required**
Must always be `Bold`. | +| `accessToken` | `string` | **Required**
Access token for the Bold API. | +| `refreshToken` | `string` | **Required**
Refresh token for the Bold API. | +| `refreshURL` | `string` | **Optional**
Custom refresh URL for token refreshing. Use this only if you authenticated with a custom backend. | +| `legacyAuthentication` | `boolean` | **Required**
Switch between default and legacy authentication. This settings will impact token refreshing. | +| | | | ## Backend -The `backend/` folder contains the source code for the backend that is used while authenticating using the Bold app (default authentication). I host this myself on AWS. While your password is never available to this server, you can choose to self host this backend if you obtain a client id and secret from Bold. Specify a custom backend by clicking the settings icon on the login page. +The `backend/` folder contains the source code for the backend that is used while authenticating using the Bold app (default authentication). I host this myself on AWS, with a client id and secret that was provided to me. While your password is never available to this server, you can choose to self host this backend if you obtain a client id and secret from Bold. Specify a custom backend by clicking the settings icon on the login page. Also specify a custom refresh URL in the config, as a custom client id and secret require refreshing with the same client id and secret. -Alternatively you can also choose to use Legacy Authentication using username/password if you prefer not to use either of these options. This will log out your Bold app as only one username/password session can be active at the same time. +Alternatively you can also choose to use legacy authentication using username/password if you prefer not to use either of these options. This will log out your Bold app as only one username/password based session can be active at the same time. + +*Note:* While default authentication is (semi-)supported by Bold, legacy authentication is not supported at all and may break at any time. ## Credits diff --git a/backend/package-lock.json b/backend/package-lock.json index ece5405..96b2b1e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,7 +24,7 @@ "serverless": "^3.16.0", "serverless-domain-manager": "^6.0.3", "serverless-dynamodb-local": "^0.2.40", - "serverless-offline": "^8.7.0", + "serverless-offline": "^8.8.0", "serverless-scriptable-plugin": "^1.2.2", "typescript": "^4.6.4" } @@ -2124,9 +2124,9 @@ } }, "node_modules/aws-sdk": { - "version": "2.1125.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1125.0.tgz", - "integrity": "sha512-2syNkKDqDcDmB/chc61a5xx+KYzaarLs1/KshE0b1Opp2oSq2FARyUBbk59HgwKaDUB61uPF33ZG9sHiIVx2hQ==", + "version": "2.1149.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1149.0.tgz", + "integrity": "sha512-wNb3YMLhXoK4UkjXhGAWMjRdrXT/Zhv3KdgPmd7VWlr3nXMViLwVJEEYdVmALUdkzCefdzY1JUTRLMgCxtn9EA==", "dev": true, "dependencies": { "buffer": "4.9.2", @@ -2136,7 +2136,7 @@ "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", - "uuid": "3.3.2", + "uuid": "8.0.0", "xml2js": "0.4.19" }, "engines": { @@ -2154,13 +2154,12 @@ } }, "node_modules/aws-sdk/node_modules/uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", "dev": true, "bin": { - "uuid": "bin/uuid" + "uuid": "dist/bin/uuid" } }, "node_modules/axios": { @@ -2974,18 +2973,27 @@ } }, "node_modules/cron-parser": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.18.0.tgz", - "integrity": "sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", + "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", "dev": true, "dependencies": { - "is-nan": "^1.3.0", - "moment-timezone": "^0.5.31" + "is-nan": "^1.3.2", + "luxon": "^1.26.0" }, "engines": { "node": ">=0.8" } }, + "node_modules/cron-parser/node_modules/luxon": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", + "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -5950,7 +5958,7 @@ "node_modules/long-timeout": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", - "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==", "dev": true }, "node_modules/loupe": { @@ -5993,12 +6001,12 @@ } }, "node_modules/luxon": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", - "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz", + "integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/make-dir": { @@ -6406,27 +6414,6 @@ "node": ">=6" } }, - "node_modules/moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.34", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", - "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", - "dev": true, - "dependencies": { - "moment": ">= 2.9.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6531,14 +6518,17 @@ } }, "node_modules/node-schedule": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.3.tgz", - "integrity": "sha512-uF9Ubn6luOPrcAYKfsXWimcJ1tPFtQ8I85wb4T3NgJQrXazEzojcFZVk46ZlLHby3eEJChgkV/0T689IsXh2Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-nl4JTiZ7ZQDc97MmpTq9BQjYhq7gOtoh7SiPH069gBFBj0PzD8HI7zyFs6rzqL8Y5tTiEEYLxgtbx034YPrbyQ==", "dev": true, "dependencies": { - "cron-parser": "^2.18.0", + "cron-parser": "^3.5.0", "long-timeout": "0.1.1", "sorted-array-functions": "^1.3.0" + }, + "engines": { + "node": ">=6" } }, "node_modules/normalize-path": { @@ -6652,23 +6642,6 @@ "node": ">= 0.4" } }, - "node_modules/object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.getownpropertydescriptors": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", @@ -7280,15 +7253,6 @@ "node": ">=0.10.0" } }, - "node_modules/please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, - "dependencies": { - "semver-compare": "^1.0.0" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7764,12 +7728,6 @@ "node": ">=10" } }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true - }, "node_modules/semver-diff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", @@ -7891,36 +7849,33 @@ } }, "node_modules/serverless-offline": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/serverless-offline/-/serverless-offline-8.7.0.tgz", - "integrity": "sha512-OqBfSFk4iuhBx6oXxLLRY7QKNVzBIZBnhgeG/4GmJ+dG93AGYts0BD6Myi5EZALCD8T6WYQiNwHQRPDyGIgz2w==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/serverless-offline/-/serverless-offline-8.8.0.tgz", + "integrity": "sha512-FLS5AZb6uGxalDfwB9/M6jl3IOA4zwIEIgJY8QANuNncbwbcoZfilD1Ajl87hLvUnRvTzncv3gz8QYssVAvq8Q==", "dev": true, "dependencies": { "@hapi/boom": "^9.1.4", "@hapi/h2o2": "^9.1.0", - "@hapi/hapi": "^20.2.1", - "aws-sdk": "^2.1097.0", + "@hapi/hapi": "^20.2.2", + "aws-sdk": "^2.1136.0", "boxen": "^5.1.2", "chalk": "^4.1.2", "cuid": "^2.1.8", "execa": "^5.1.1", - "extend": "^3.0.2", - "fs-extra": "^9.1.0", + "fs-extra": "^10.1.0", "java-invoke-local": "0.0.6", "js-string-escape": "^1.0.1", "jsonpath-plus": "^5.1.0", "jsonschema": "^1.4.0", "jsonwebtoken": "^8.5.1", - "jszip": "^3.7.1", - "luxon": "^1.28.0", + "jszip": "^3.9.1", + "luxon": "^2.4.0", "node-fetch": "^2.6.7", - "node-schedule": "^1.3.3", - "object.fromentries": "^2.0.5", + "node-schedule": "^2.1.0", "p-memoize": "^4.0.4", "p-queue": "^6.6.2", - "p-retry": "^4.6.1", - "please-upgrade-node": "^3.2.0", - "semver": "^7.3.5", + "p-retry": "^4.6.2", + "semver": "^7.3.7", "update-notifier": "^5.1.0", "velocityjs": "^2.0.6", "ws": "^7.5.7" @@ -7932,6 +7887,20 @@ "serverless": "^1.60.0 || 2 || 3" } }, + "node_modules/serverless-offline/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/serverless-scriptable-plugin": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/serverless-scriptable-plugin/-/serverless-scriptable-plugin-1.2.2.tgz", @@ -10913,9 +10882,9 @@ "dev": true }, "aws-sdk": { - "version": "2.1125.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1125.0.tgz", - "integrity": "sha512-2syNkKDqDcDmB/chc61a5xx+KYzaarLs1/KshE0b1Opp2oSq2FARyUBbk59HgwKaDUB61uPF33ZG9sHiIVx2hQ==", + "version": "2.1149.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1149.0.tgz", + "integrity": "sha512-wNb3YMLhXoK4UkjXhGAWMjRdrXT/Zhv3KdgPmd7VWlr3nXMViLwVJEEYdVmALUdkzCefdzY1JUTRLMgCxtn9EA==", "dev": true, "requires": { "buffer": "4.9.2", @@ -10925,7 +10894,7 @@ "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", - "uuid": "3.3.2", + "uuid": "8.0.0", "xml2js": "0.4.19" }, "dependencies": { @@ -10936,9 +10905,9 @@ "dev": true }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", "dev": true } } @@ -11573,13 +11542,21 @@ } }, "cron-parser": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-2.18.0.tgz", - "integrity": "sha512-s4odpheTyydAbTBQepsqd2rNWGa2iV3cyo8g7zbI2QQYGLVsfbhmwukayS1XHppe02Oy1fg7mg6xoaraVJeEcg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-3.5.0.tgz", + "integrity": "sha512-wyVZtbRs6qDfFd8ap457w3XVntdvqcwBGxBoTvJQH9KGVKL/fB+h2k3C8AqiVxvUQKN1Ps/Ns46CNViOpVDhfQ==", "dev": true, "requires": { - "is-nan": "^1.3.0", - "moment-timezone": "^0.5.31" + "is-nan": "^1.3.2", + "luxon": "^1.26.0" + }, + "dependencies": { + "luxon": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", + "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==", + "dev": true + } } }, "cross-spawn": { @@ -13879,7 +13856,7 @@ "long-timeout": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", - "integrity": "sha1-lyHXiLR+C8taJMLivuGg2lXatRQ=", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==", "dev": true }, "loupe": { @@ -13916,9 +13893,9 @@ } }, "luxon": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", - "integrity": "sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.4.0.tgz", + "integrity": "sha512-w+NAwWOUL5hO0SgwOHsMBAmZ15SoknmQXhSO0hIbJCAmPKSsGeK8MlmhYh2w6Iib38IxN2M+/ooXWLbeis7GuA==", "dev": true }, "make-dir": { @@ -14242,21 +14219,6 @@ } } }, - "moment": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", - "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", - "dev": true - }, - "moment-timezone": { - "version": "0.5.34", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", - "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", - "dev": true, - "requires": { - "moment": ">= 2.9.0" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -14346,12 +14308,12 @@ } }, "node-schedule": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-1.3.3.tgz", - "integrity": "sha512-uF9Ubn6luOPrcAYKfsXWimcJ1tPFtQ8I85wb4T3NgJQrXazEzojcFZVk46ZlLHby3eEJChgkV/0T689IsXh2Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-nl4JTiZ7ZQDc97MmpTq9BQjYhq7gOtoh7SiPH069gBFBj0PzD8HI7zyFs6rzqL8Y5tTiEEYLxgtbx034YPrbyQ==", "dev": true, "requires": { - "cron-parser": "^2.18.0", + "cron-parser": "^3.5.0", "long-timeout": "0.1.1", "sorted-array-functions": "^1.3.0" } @@ -14436,17 +14398,6 @@ "object-keys": "^1.0.11" } }, - "object.fromentries": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", - "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.1" - } - }, "object.getownpropertydescriptors": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz", @@ -14899,15 +14850,6 @@ "pinkie": "^2.0.0" } }, - "please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", - "dev": true, - "requires": { - "semver-compare": "^1.0.0" - } - }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -15231,12 +15173,6 @@ "lru-cache": "^6.0.0" } }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true - }, "semver-diff": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", @@ -15337,39 +15273,49 @@ } }, "serverless-offline": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/serverless-offline/-/serverless-offline-8.7.0.tgz", - "integrity": "sha512-OqBfSFk4iuhBx6oXxLLRY7QKNVzBIZBnhgeG/4GmJ+dG93AGYts0BD6Myi5EZALCD8T6WYQiNwHQRPDyGIgz2w==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/serverless-offline/-/serverless-offline-8.8.0.tgz", + "integrity": "sha512-FLS5AZb6uGxalDfwB9/M6jl3IOA4zwIEIgJY8QANuNncbwbcoZfilD1Ajl87hLvUnRvTzncv3gz8QYssVAvq8Q==", "dev": true, "requires": { "@hapi/boom": "^9.1.4", "@hapi/h2o2": "^9.1.0", - "@hapi/hapi": "^20.2.1", - "aws-sdk": "^2.1097.0", + "@hapi/hapi": "^20.2.2", + "aws-sdk": "^2.1136.0", "boxen": "^5.1.2", "chalk": "^4.1.2", "cuid": "^2.1.8", "execa": "^5.1.1", - "extend": "^3.0.2", - "fs-extra": "^9.1.0", + "fs-extra": "^10.1.0", "java-invoke-local": "0.0.6", "js-string-escape": "^1.0.1", "jsonpath-plus": "^5.1.0", "jsonschema": "^1.4.0", "jsonwebtoken": "^8.5.1", - "jszip": "^3.7.1", - "luxon": "^1.28.0", + "jszip": "^3.9.1", + "luxon": "^2.4.0", "node-fetch": "^2.6.7", - "node-schedule": "^1.3.3", - "object.fromentries": "^2.0.5", + "node-schedule": "^2.1.0", "p-memoize": "^4.0.4", "p-queue": "^6.6.2", - "p-retry": "^4.6.1", - "please-upgrade-node": "^3.2.0", - "semver": "^7.3.5", + "p-retry": "^4.6.2", + "semver": "^7.3.7", "update-notifier": "^5.1.0", "velocityjs": "^2.0.6", "ws": "^7.5.7" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } } }, "serverless-scriptable-plugin": { diff --git a/backend/package.json b/backend/package.json index c06fac2..515975e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,13 +6,14 @@ "scripts": { "dev": "serverless offline start --stage dev", "build": "tsc", - "package": "npm run prepackage && npm run package:websocket-manager && npm run package:oauth-begin && npm run package:oauth-callback && npm run postpackage", "db:migrate": "serverless dynamodb migrate --stage dev", "clean": "rm -rf build/ dist/", "prepackage": "npm run clean && mkdir -p build dist && npm run build && cp package*.json build/ && cd build && npm install --production", + "package": "npm run prepackage && npm run package:websocket-manager && npm run package:oauth-begin && npm run package:oauth-callback && npm run package:oauth-refresh && npm run postpackage", "package:websocket-manager": "cd build && zip -r ../dist/websocket-manager.zip websocket-manager/ common/ node_modules/ package*.json", "package:oauth-begin": "cd build && zip -r ../dist/oauth-begin.zip oauth-begin.js common/ node_modules/ package*.json", "package:oauth-callback": "cd build && zip -r ../dist/oauth-callback.zip oauth-callback.js common/ node_modules/ package*.json", + "package:oauth-refresh": "cd build && zip -r ../dist/oauth-refresh.zip oauth-refresh.js common/ node_modules/ package*.json", "postpackage": "rm -rf build/" }, "keywords": [ @@ -30,7 +31,7 @@ "serverless": "^3.16.0", "serverless-domain-manager": "^6.0.3", "serverless-dynamodb-local": "^0.2.40", - "serverless-offline": "^8.7.0", + "serverless-offline": "^8.8.0", "serverless-scriptable-plugin": "^1.2.2", "typescript": "^4.6.4" }, diff --git a/backend/serverless.yaml b/backend/serverless.yaml index 57cccd8..6ece5e9 100644 --- a/backend/serverless.yaml +++ b/backend/serverless.yaml @@ -89,6 +89,20 @@ functions: method: GET path: /oauth/callback + oauthRefresh: + handler: oauth-refresh.handler + name: ${self:service}-${opt:stage}-oauth-refresh + + environment: ${self:custom.stageOptions.environment.${opt:stage}, self:custom.stageOptions.environment.default} + + package: + artifact: dist/oauth-refresh.zip + + events: + - httpApi: + method: POST + path: /oauth/refresh + resources: Resources: ConnectionsTable: diff --git a/backend/src/common/respones.ts b/backend/src/common/respones.ts index d196cd8..77ff626 100644 --- a/backend/src/common/respones.ts +++ b/backend/src/common/respones.ts @@ -1,15 +1,19 @@ import { APIGatewayProxyResult } from 'aws-lambda'; -export function respone(statusCode: number, data?: Record): APIGatewayProxyResult { +export function response(statusCode: number, string?: string): APIGatewayProxyResult +export function response(statusCode: number, data?: Record): APIGatewayProxyResult + +export function response(statusCode: number, stringOrData?: string | Record): APIGatewayProxyResult { return { statusCode, headers: { 'Content-Type': 'application/json' }, - body: data ? JSON.stringify({ - success: true, - data - }) : '' + body: typeof stringOrData == 'string' ? stringOrData : + stringOrData != null ? JSON.stringify({ + success: true, + data: stringOrData + }) : '' }; } diff --git a/backend/src/oauth-begin.ts b/backend/src/oauth-begin.ts index 767c523..350440a 100644 --- a/backend/src/oauth-begin.ts +++ b/backend/src/oauth-begin.ts @@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid'; import { dynamoClient, websocketClient } from './common/clients'; import { environment } from './common/environment'; -import { errorResponse, internalErrorResponse, respone } from './common/respones'; +import { errorResponse, internalErrorResponse, response } from './common/respones'; export const handler: APIGatewayProxyHandler = async (event): Promise => { try { @@ -70,7 +70,7 @@ export const handler: APIGatewayProxyHandler = async (event): Promise => { try { @@ -124,7 +124,7 @@ export const handler: APIGatewayProxyHandlerV2 = async (event): Promise => { + try { + let { body: bodyString } = event; + + let { refreshToken } = JSON.parse(bodyString || '{}'); + + if (refreshToken == null) { + return errorResponse(400, 'Missing refresh token'); + } + + let accessToken, newRefreshToken; + + try { + let form = new URLSearchParams(); + + form.append('grant_type', 'refresh_token'); + form.append('refresh_token', refreshToken); + form.append('client_id', environment.BOLD_CLIENT_ID || ''); + form.append('client_secret', environment.BOLD_CLIENT_SECRET || ''); + + let authResponse = await axios.post('https://api.boldsmartlock.com/v2/oauth/token', form.toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + + accessToken = authResponse.data.access_token; + newRefreshToken = authResponse.data.refresh_token; + } catch (error) { + console.error(`Error while refreshing access token: ${error}`); + + if (axios.isAxiosError(error)) { + console.error(error.response?.data); + + return errorResponse(error.response?.status ?? 500, `Bold Error: ${JSON.stringify(error.response?.data ?? '"Unknown error"')}`); + } + + return internalErrorResponse(`Error while refreshing access token: ${error}`); + } + + if (!accessToken || !newRefreshToken) { + console.error('Missing access or new refresh token'); + return internalErrorResponse('Missing access or new refresh token'); + } + + return response(200, { accessToken, refreshToken: newRefreshToken }); + } catch (error) { + console.error(`Unhandled exception: ${error}`); + return internalErrorResponse(`Unhandled exception: ${error}`); + } +}; \ No newline at end of file diff --git a/backend/src/websocket-manager/routes.ts b/backend/src/websocket-manager/routes.ts index 9184c6e..0f904c7 100644 --- a/backend/src/websocket-manager/routes.ts +++ b/backend/src/websocket-manager/routes.ts @@ -2,7 +2,7 @@ import { DeleteItemCommand, PutItemCommand } from '@aws-sdk/client-dynamodb'; import type { APIGatewayProxyResult } from 'aws-lambda'; import { dynamoClient } from '../common/clients'; -import { internalErrorResponse, respone } from '../common/respones'; +import { internalErrorResponse, response } from '../common/respones'; import { environment } from '../common/environment'; export async function connect(connectionId: string): Promise { @@ -19,7 +19,7 @@ export async function connect(connectionId: string): Promise { @@ -36,5 +36,5 @@ export async function disconnect(connectionId: string): Promise", - "refreshToken": "" + "refreshToken": "", + "legacyAuthentication": "false" } ] } \ No newline at end of file diff --git a/plugin/config.schema.json b/plugin/config.schema.json index e75f367..8e95885 100644 --- a/plugin/config.schema.json +++ b/plugin/config.schema.json @@ -5,6 +5,7 @@ "customUi": true, "schema": { "type": "object", + "required": ["accessToken", "refreshToken", "legacyAuthentication"], "properties": { "accessToken": { "title": "Access token", @@ -17,6 +18,16 @@ "type": "string", "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", "description": "This token will be used to refresh the access token." + }, + "refreshURL": { + "title": "Custom refresh URL", + "type": "string", + "description": "Custom refresh URL for token refreshing. Use this only if you authenticated with a custom backend." + }, + "legacyAuthentication": { + "title": "Use legacy authentication", + "type": "boolean", + "description": "Switch between default and legacy authentication. This settings will impact token refreshing." } } } diff --git a/plugin/homebridge-ui/src/components/App/App.tsx b/plugin/homebridge-ui/src/components/App/App.tsx index 867ad6c..10138c3 100644 --- a/plugin/homebridge-ui/src/components/App/App.tsx +++ b/plugin/homebridge-ui/src/components/App/App.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import QRCode from 'react-qr-code'; import AuthManager from '../../auth-manager'; +import { Config } from '../../types'; import LegacyAuth from '../LegacyAuth'; import Page from '../Page'; @@ -13,7 +14,7 @@ function App(): JSX.Element { let [isShowingQRCode, setShowingQRCode] = useState(false); let [oauthURL, setOAuthURL] = useState(); - let [result, setResult] = useState<{ accessToken: string, refreshToken: string }>(); + let [result, setResult] = useState(); let [isConfigured, setConfigured] = useState(); let [isInSettings, setInSettings] = useState(false); @@ -40,7 +41,7 @@ function App(): JSX.Element { setOAuthURL(`https://boldsmartlock.com/app/authorize?response_type=code&client_id=HomeBridge&redirect_uri=${encodeURI(`${AuthManager.shared.callbackURL}`)}&state=${encodeURI(callbackId)}`); AuthManager.shared.once('oauthCallback', (result) => { - setResult(result); + setResult({ ...result, legacyAuthentication: false }); AuthManager.shared.close(); }); } @@ -71,6 +72,7 @@ function App(): JSX.Element { config[0].accessToken = result.accessToken; config[0].refreshToken = result.refreshToken; + config[0].legacyAuthentication = result.legacyAuthentication; await homebridge.updatePluginConfig(config); homebridge.showSchemaForm(); @@ -104,8 +106,8 @@ function App(): JSX.Element { - - + + ); } else if (!isAuthenticating) { diff --git a/plugin/homebridge-ui/src/components/LegacyAuth/LegacyAuth.tsx b/plugin/homebridge-ui/src/components/LegacyAuth/LegacyAuth.tsx index c823157..8273ae7 100644 --- a/plugin/homebridge-ui/src/components/LegacyAuth/LegacyAuth.tsx +++ b/plugin/homebridge-ui/src/components/LegacyAuth/LegacyAuth.tsx @@ -1,9 +1,9 @@ import React, { useState } from 'react'; -import { LegacyAuthPage } from '../../types'; +import { Config, LegacyAuthPage } from '../../types'; import Page from '../Page'; interface LegacyAuthProps { - setResult: (result: { accessToken: string, refreshToken: string }) => void; + setResult: (result: Config) => void; } function LegacyAuth(props: LegacyAuthProps): JSX.Element { @@ -150,7 +150,7 @@ function LegacyAuth(props: LegacyAuthProps): JSX.Element { let { access_token, refresh_token } = body; - props.setResult({ accessToken: access_token, refreshToken: refresh_token }); + props.setResult({ accessToken: access_token, refreshToken: refresh_token, legacyAuthentication: true }); } catch (error) { console.error(`Error while authenticating: ${error}`); setError((error as any).toString()); diff --git a/plugin/homebridge-ui/src/types/config.ts b/plugin/homebridge-ui/src/types/config.ts new file mode 100644 index 0000000..291d45e --- /dev/null +++ b/plugin/homebridge-ui/src/types/config.ts @@ -0,0 +1,5 @@ +export interface Config { + accessToken: string; + refreshToken: string; + legacyAuthentication: boolean; +} \ No newline at end of file diff --git a/plugin/homebridge-ui/src/types/index.ts b/plugin/homebridge-ui/src/types/index.ts index eae448a..5aac783 100644 --- a/plugin/homebridge-ui/src/types/index.ts +++ b/plugin/homebridge-ui/src/types/index.ts @@ -1 +1,2 @@ -export * from './page'; \ No newline at end of file +export * from './page'; +export * from './config'; \ No newline at end of file diff --git a/plugin/package-lock.json b/plugin/package-lock.json index 180a928..0b8e92c 100644 --- a/plugin/package-lock.json +++ b/plugin/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-bold", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "homebridge-bold", - "version": "2.0.0", + "version": "2.1.0", "funding": [ { "type": "paypal", diff --git a/plugin/package.json b/plugin/package.json index 96bd38e..2f96658 100644 --- a/plugin/package.json +++ b/plugin/package.json @@ -1,7 +1,7 @@ { "name": "homebridge-bold", "displayName": "Homebridge Bold", - "version": "2.0.0", + "version": "2.1.0", "description": "HomeKit support for the Bold Smart Locks.", "main": "build/index.js", "engines": { diff --git a/plugin/src/bold.ts b/plugin/src/bold.ts index c8b3df3..2781cad 100644 --- a/plugin/src/bold.ts +++ b/plugin/src/bold.ts @@ -1,7 +1,8 @@ import axios, { AxiosError, Method } from 'axios'; import FormData from 'form-data'; import { Logger } from 'homebridge'; -import { Device } from './types'; +import { REFRESH_URL, LEGACY_CLIENT_ID, LEGACY_CLIENT_SECRET } from './const'; +import { Config, Device } from './types'; interface APISuccess { success: true; @@ -21,8 +22,7 @@ type APIResponse = APISuccess | APIError; export class BoldAPI { constructor( - private accessToken: string, - private refreshToken: string, + private config: Config, private log: Logger ) {} @@ -32,7 +32,7 @@ export class BoldAPI { method: method, url: `https://api.sesamtechnology.com${endpoint}`, headers: { - 'Authorization': `Bearer ${this.accessToken}`, + 'Authorization': `Bearer ${this.config.accessToken}`, ...(!Object.keys(headers || {}).some((header) => header.toLowerCase() == 'content-type') && { 'Content-Type': 'application/json' }), ...headers }, @@ -117,11 +117,40 @@ export class BoldAPI { async refresh(): Promise<{ accessToken: string, refreshToken: string } | undefined> { this.log.debug('Refreshing access token'); + if (this.config.legacyAuthentication) { + return await this.refreshLegacy(); + } + + try { + let response = await axios.post(this.config.refreshURL || REFRESH_URL, { refreshToken: this.config.refreshToken }); + + let { accessToken, refreshToken } = response.data.data; + + if (!accessToken || !refreshToken) { + this.log.error(`Missing access or refresh token: ${JSON.stringify(response.data)}`); + return; + } + + this.log.debug('Successfully refreshed access token'); + + return { accessToken, refreshToken }; + } catch (error) { + if (axios.isAxiosError(error)) { + let axiosError = error as AxiosError; + + this.log.error(`Error (${error.response?.status}) while refreshing token: ${axiosError.response?.data?.error?.message || error}`); + } else { + this.log.error(`Error while refreshing access token: ${error}`); + } + } + } + + async refreshLegacy(): Promise<{ accessToken: string, refreshToken: string } | undefined> { let formData = new FormData(); - formData.append('client_id', 'BoldApp'); - formData.append('client_secret', 'pgJFgnGB87f9ednFiiHygCbf'); - formData.append('refresh_token', this.refreshToken); + formData.append('client_id', LEGACY_CLIENT_ID); + formData.append('client_secret', LEGACY_CLIENT_SECRET); + formData.append('refresh_token', this.config.refreshToken); formData.append('grant_type', 'refresh_token'); let response = await this.request('POST', '/v2/oauth/token', formData, formData.getHeaders()); @@ -134,11 +163,11 @@ export class BoldAPI { let data = response.data as any; - this.accessToken = data.access_token; - this.refreshToken = data.refresh_token; + this.config.accessToken = data.access_token; + this.config.refreshToken = data.refresh_token; this.log.debug('Successfully refreshed access token'); - return { accessToken: this.accessToken, refreshToken: this.refreshToken }; + return { accessToken: this.config.accessToken, refreshToken: this.config.refreshToken }; } } \ No newline at end of file diff --git a/plugin/src/const.ts b/plugin/src/const.ts index f2efa22..986bb84 100644 --- a/plugin/src/const.ts +++ b/plugin/src/const.ts @@ -1,2 +1,6 @@ export const PLUGIN_NAME = 'homebridge-bold'; -export const PLATFORM_NAME = 'Bold'; \ No newline at end of file +export const PLATFORM_NAME = 'Bold'; + +export const REFRESH_URL = 'https://bold.nienhuisdevelopment.com/oauth/refresh'; +export const LEGACY_CLIENT_ID = 'BoldApp'; +export const LEGACY_CLIENT_SECRET = 'pgJFgnGB87f9ednFiiHygCbf'; \ No newline at end of file diff --git a/plugin/src/index.ts b/plugin/src/index.ts index b26d1e1..a26c866 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -28,7 +28,7 @@ class BoldPlatform implements DynamicPlatformPlugin { ) { this.hap = api.hap; this.config = config as Config; - this.bold = new BoldAPI(this.config.accessToken, this.config.refreshToken, this.log); + this.bold = new BoldAPI({ ...this.config }, this.log); api.on(APIEvent.DID_FINISH_LAUNCHING, async () => { await this.refreshAccessToken(); diff --git a/plugin/src/types/config.ts b/plugin/src/types/config.ts index af89854..2398e39 100644 --- a/plugin/src/types/config.ts +++ b/plugin/src/types/config.ts @@ -3,4 +3,6 @@ import { PlatformConfig } from 'homebridge'; export interface Config extends PlatformConfig { accessToken: string; refreshToken: string; + refreshURL?: string; + legacyAuthentication: boolean; } \ No newline at end of file