From 3b7a206cec4586c6ab92e743b7fc9929aaafc001 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 14:25:36 -0400 Subject: [PATCH] Add an authorize page for authentication (#1147) * Use authorize page if auth provider * Add webcomponent polyfill * More fixes * ES5 fix * Lint * Use redirect_uri * upgrade uglify to fix tests? * Update browsers used for testing --- .gitignore | 1 + gulp/tasks/auth.js | 23 +++ index.html | 1 + js/common/auth/fetch_token.js | 15 ++ js/common/auth/refresh_token.js | 15 ++ js/common/util/parse_query.js | 11 ++ js/core.js | 74 ++++++++- package.json | 2 +- .../config/config-entries/ha-config-flow.html | 10 +- script/build_frontend | 1 + src/auth/ha-auth-flow.html | 143 ++++++++++++++++++ src/auth/ha-authorize.html | 81 ++++++++++ src/auth/ha-pick-auth-provider.html | 86 +++++++++++ src/authorize.html | 26 ++++ .../components}/ha-form.html | 24 +-- src/home-assistant.html | 42 +++-- src/util/hass-call-api.html | 2 + test-mocha/common/util/parse_query_test.js | 18 +++ wct.conf.json | 4 +- yarn.lock | 26 ++-- 20 files changed, 550 insertions(+), 55 deletions(-) create mode 100644 gulp/tasks/auth.js create mode 100644 js/common/auth/fetch_token.js create mode 100644 js/common/auth/refresh_token.js create mode 100644 js/common/util/parse_query.js create mode 100644 src/auth/ha-auth-flow.html create mode 100644 src/auth/ha-authorize.html create mode 100644 src/auth/ha-pick-auth-provider.html create mode 100644 src/authorize.html rename {panels/config/config-entries => src/components}/ha-form.html (81%) create mode 100644 test-mocha/common/util/parse_query_test.js diff --git a/.gitignore b/.gitignore index 8f5f200290..866eca9a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ dist # Secrets .lokalise_token +yarn-error.log diff --git a/gulp/tasks/auth.js b/gulp/tasks/auth.js new file mode 100644 index 0000000000..4fd417ee0e --- /dev/null +++ b/gulp/tasks/auth.js @@ -0,0 +1,23 @@ +const gulp = require('gulp'); +const replace = require('gulp-batch-replace'); +const rename = require('gulp-rename'); + +const config = require('../config'); +const minifyStream = require('../common/transform').minifyStream; +const { + bundledStreamFromHTML, +} = require('../common/html'); + +const es5Extra = ""; + +async function buildAuth(es6) { + let stream = await bundledStreamFromHTML('src/authorize.html'); + stream = stream.pipe(replace([['', es6 ? '' : es5Extra]])); + + return minifyStream(stream, /* es6= */ es6) + .pipe(rename('authorize.html')) + .pipe(gulp.dest(es6 ? config.output : config.output_es5)); +} + +gulp.task('authorize-es5', () => buildAuth(/* es6= */ false)); +gulp.task('authorize', () => buildAuth(/* es6= */ true)); diff --git a/index.html b/index.html index e26d966fac..a5b8ad32f7 100644 --- a/index.html +++ b/index.html @@ -61,6 +61,7 @@ document.getElementById('ha-init-skeleton').classList.add('error'); }; window.noAuth = '{{ no_auth }}'; + window.clientId = '{{ client_id }}' window.Polymer = { lazyRegister: true, useNativeCSSProperties: true, diff --git a/js/common/auth/fetch_token.js b/js/common/auth/fetch_token.js new file mode 100644 index 0000000000..17c47fa8a7 --- /dev/null +++ b/js/common/auth/fetch_token.js @@ -0,0 +1,15 @@ +export default function fetchToken(clientId, code) { + const data = new FormData(); + data.append('grant_type', 'authorization_code'); + data.append('code', code); + return fetch('/auth/token', { + method: 'POST', + headers: { + authorization: `Basic ${btoa(clientId)}` + }, + body: data, + }).then((resp) => { + if (!resp.ok) throw new Error('Unable to fetch tokens'); + return resp.json(); + }); +} diff --git a/js/common/auth/refresh_token.js b/js/common/auth/refresh_token.js new file mode 100644 index 0000000000..d7d20e4a97 --- /dev/null +++ b/js/common/auth/refresh_token.js @@ -0,0 +1,15 @@ +export default function refreshAccessToken(clientId, refreshToken) { + const data = new FormData(); + data.append('grant_type', 'refresh_token'); + data.append('refresh_token', refreshToken); + return fetch('/auth/token', { + method: 'POST', + headers: { + authorization: `Basic ${btoa(clientId)}` + }, + body: data, + }).then((resp) => { + if (!resp.ok) throw new Error('Unable to fetch tokens'); + return resp.json(); + }); +} diff --git a/js/common/util/parse_query.js b/js/common/util/parse_query.js new file mode 100644 index 0000000000..b654b0172f --- /dev/null +++ b/js/common/util/parse_query.js @@ -0,0 +1,11 @@ +export default function parseQuery(queryString) { + const query = {}; + const items = queryString.split('&'); + for (let i = 0; i < items.length; i++) { + const item = items[i].split('='); + const key = decodeURIComponent(item[0]); + const value = item.length > 1 ? decodeURIComponent(item[1]) : undefined; + query[key] = value; + } + return query; +} diff --git a/js/core.js b/js/core.js index 31a0434dd4..6e9f0bc484 100644 --- a/js/core.js +++ b/js/core.js @@ -1,21 +1,26 @@ import * as HAWS from 'home-assistant-js-websocket'; +import fetchToken from './common/auth/fetch_token.js'; +import refreshToken_ from './common/auth/refresh_token.js'; +import parseQuery from './common/util/parse_query.js'; + window.HAWS = HAWS; window.HASS_DEMO = __DEMO__; window.HASS_DEV = __DEV__; window.HASS_BUILD = __BUILD__; window.HASS_VERSION = __VERSION__; -const init = window.createHassConnection = function (password) { +const init = window.createHassConnection = function (password, accessToken) { const proto = window.location.protocol === 'https:' ? 'wss' : 'ws'; const url = `${proto}://${window.location.host}/api/websocket?${window.HASS_BUILD}`; const options = { setupRetry: 10, }; - if (password !== undefined) { + if (password) { options.authToken = password; + } else if (accessToken) { + options.accessToken = accessToken; } - return HAWS.createConnection(url, options) .then(function (conn) { HAWS.subscribeEntities(conn); @@ -24,12 +29,65 @@ const init = window.createHassConnection = function (password) { }); }; -if (window.noAuth === '1') { - window.hassConnection = init(); -} else if (window.localStorage.authToken) { - window.hassConnection = init(window.localStorage.authToken); +function redirectLogin() { + const urlBase = __DEV__ ? '/home-assistant-polymer/src' : `/frontend_${__BUILD__}`; + document.location = `${urlBase}/authorize.html?response_type=code&client_id=${window.clientId}&redirect_uri=/`; +} + +window.refreshToken = () => + refreshToken_(window.clientId, window.tokens.refresh_token).then((accessTokenResp) => { + window.tokens.access_token = accessTokenResp.access_token; + localStorage.tokens = JSON.stringify(window.tokens); + return accessTokenResp.access_token; + }, () => redirectLogin()); + +function resolveCode(code) { + fetchToken(window.clientId, code).then((tokens) => { + localStorage.tokens = JSON.stringify(tokens); + // Refresh the page and have tokens in place. + document.location = location.pathname; + }, (err) => { + // eslint-disable-next-line + console.error('Resolve token failed', err); + alert('Unable to fetch tokens'); + redirectLogin(); + }); +} + +function main() { + if (location.search) { + const query = parseQuery(location.search.substr(1)); + if (query.code) { + resolveCode(query.code); + return; + } + } + if (localStorage.tokens) { + window.tokens = JSON.parse(localStorage.tokens); + window.hassConnection = init(null, window.tokens.access_token).catch((err) => { + if (err !== HAWS.ERR_INVALID_AUTH) throw err; + + return window.refreshToken().then(accessToken => init(null, accessToken)); + }); + return; + } + redirectLogin(); +} + +function mainLegacy() { + if (window.noAuth === '1') { + window.hassConnection = init(); + } else if (window.localStorage.authToken) { + window.hassConnection = init(window.localStorage.authToken); + } else { + window.hassConnection = null; + } +} + +if (window.clientId) { + main(); } else { - window.hassConnection = null; + mainLegacy(); } window.addEventListener('error', (e) => { diff --git a/package.json b/package.json index 846e55e2e7..80681adec2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "dependencies": { "es6-object-assign": "^1.1.0", "fecha": "^2.3.3", - "home-assistant-js-websocket": "^1.1.2", + "home-assistant-js-websocket": "1.2.0", "mdn-polyfills": "^5.5.0", "preact": "^8.2.6", "unfetch": "^3.0.0" diff --git a/panels/config/config-entries/ha-config-flow.html b/panels/config/config-entries/ha-config-flow.html index eb4bb141b5..439602e4c2 100644 --- a/panels/config/config-entries/ha-config-flow.html +++ b/panels/config/config-entries/ha-config-flow.html @@ -6,7 +6,7 @@ - + - - localize(`component.${step.handler}.config.step.${step.step_id}.data.${schema.name}`); diff --git a/script/build_frontend b/script/build_frontend index afb778d8f2..33b92b59ed 100755 --- a/script/build_frontend +++ b/script/build_frontend @@ -16,6 +16,7 @@ cp -r public/__init__.py $OUTPUT_DIR_ES5/ # Build frontend BUILD_DEV=0 ./node_modules/.bin/gulp +BUILD_DEV=0 ./node_modules/.bin/gulp authorize authorize-es5 # Entry points cp build/*.js build/*.html $OUTPUT_DIR diff --git a/src/auth/ha-auth-flow.html b/src/auth/ha-auth-flow.html new file mode 100644 index 0000000000..5ab9ecf8db --- /dev/null +++ b/src/auth/ha-auth-flow.html @@ -0,0 +1,143 @@ + + + + + + + + + + + diff --git a/src/auth/ha-authorize.html b/src/auth/ha-authorize.html new file mode 100644 index 0000000000..0576bd1ac2 --- /dev/null +++ b/src/auth/ha-authorize.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + diff --git a/src/auth/ha-pick-auth-provider.html b/src/auth/ha-pick-auth-provider.html new file mode 100644 index 0000000000..c60a795b71 --- /dev/null +++ b/src/auth/ha-pick-auth-provider.html @@ -0,0 +1,86 @@ + + + + + + + + + + diff --git a/src/authorize.html b/src/authorize.html new file mode 100644 index 0000000000..0517a8d3d1 --- /dev/null +++ b/src/authorize.html @@ -0,0 +1,26 @@ + + + + + Home Assistant + + + + Loading + + + + diff --git a/panels/config/config-entries/ha-form.html b/src/components/ha-form.html similarity index 81% rename from panels/config/config-entries/ha-form.html rename to src/components/ha-form.html index cb54f6baa7..c2e2fba804 100644 --- a/panels/config/config-entries/ha-form.html +++ b/src/components/ha-form.html @@ -1,13 +1,13 @@ - + - - - - - + + + + + - - + + @@ -18,6 +18,10 @@ }