Ask to store auth (#1568)

* Ask to store auth

* Reorg auth
This commit is contained in:
Paulus Schoutsen 2018-08-13 21:10:39 +02:00 committed by GitHub
parent 63c7c55843
commit 310299367b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 223 additions and 99 deletions

View File

@ -1,17 +0,0 @@
export default function fetchToken(clientId, code) {
const data = new FormData();
data.append('client_id', clientId);
data.append('grant_type', 'authorization_code');
data.append('code', code);
return fetch('/auth/token', {
credentials: 'same-origin',
method: 'POST',
body: data,
}).then((resp) => {
if (!resp.ok) throw new Error('Unable to fetch tokens');
return resp.json().then((tokens) => {
tokens.expires = (tokens.expires_in * 1000) + Date.now();
return tokens;
});
});
}

View File

@ -1,17 +0,0 @@
export default function refreshAccessToken(clientId, refreshToken) {
const data = new FormData();
data.append('client_id', clientId);
data.append('grant_type', 'refresh_token');
data.append('refresh_token', refreshToken);
return fetch('/auth/token', {
credentials: 'same-origin',
method: 'POST',
body: data,
}).then((resp) => {
if (!resp.ok) throw new Error('Unable to fetch tokens');
return resp.json().then((tokens) => {
tokens.expires = (tokens.expires_in * 1000) + Date.now();
return tokens;
});
});
}

76
src/common/auth/token.js Normal file
View File

@ -0,0 +1,76 @@
import { storeTokens, loadTokens } from './token_storage.js';
function genClientId() {
return `${location.protocol}//${location.host}/`;
}
export function redirectLogin() {
document.location.href = `/auth/authorize?response_type=code&client_id=${encodeURIComponent(genClientId())}&redirect_uri=${encodeURIComponent(location.toString())}`;
return new Promise((() => {}));
}
function fetchTokenRequest(code) {
const data = new FormData();
data.append('client_id', genClientId());
data.append('grant_type', 'authorization_code');
data.append('code', code);
return fetch('/auth/token', {
credentials: 'same-origin',
method: 'POST',
body: data,
}).then((resp) => {
if (!resp.ok) throw new Error('Unable to fetch tokens');
return resp.json().then((tokens) => {
tokens.expires = (tokens.expires_in * 1000) + Date.now();
return tokens;
});
});
}
function refreshTokenRequest(tokens) {
const data = new FormData();
data.append('client_id', genClientId());
data.append('grant_type', 'refresh_token');
data.append('refresh_token', tokens.refresh_token);
return fetch('/auth/token', {
credentials: 'same-origin',
method: 'POST',
body: data,
}).then((resp) => {
if (!resp.ok) throw new Error('Unable to fetch tokens');
return resp.json().then((newTokens) => {
newTokens.expires = (newTokens.expires_in * 1000) + Date.now();
return newTokens;
});
});
}
export function resolveCode(code) {
return fetchTokenRequest(code).then((tokens) => {
storeTokens(tokens);
history.replaceState(null, null, location.pathname);
return tokens;
}, (err) => {
// eslint-disable-next-line
console.error('Resolve token failed', err);
alert('Unable to fetch tokens');
redirectLogin();
});
}
export function refreshToken() {
const tokens = loadTokens();
if (tokens === null) {
return redirectLogin();
}
return refreshTokenRequest(tokens).then((accessTokenResp) => {
const newTokens = Object.assign({}, tokens, accessTokenResp);
storeTokens(newTokens);
return newTokens;
}, () => redirectLogin());
}

View File

@ -0,0 +1,45 @@
const storage = window.localStorage || {};
// So that core.js and main app hit same shared object.
let tokenCache = window.__tokenCache;
if (!tokenCache) {
tokenCache = window.__tokenCache = {
tokens: undefined,
writeEnabled: undefined,
};
}
export function askWrite() {
return tokenCache.writeEnabled === undefined;
}
export function storeTokens(tokens) {
tokenCache.tokens = tokens;
if (tokenCache.writeEnabled) {
try {
storage.tokens = JSON.stringify(tokens);
} catch (err) {} // eslint-disable-line
}
}
export function enableWrite() {
tokenCache.writeEnabled = true;
storeTokens(tokenCache.tokens);
}
export function loadTokens() {
if (tokenCache.tokens === undefined) {
try {
const tokens = storage.tokens;
if (tokens) {
tokenCache.tokens = JSON.parse(tokens);
tokenCache.writeEnabled = true;
} else {
tokenCache.tokens = null;
}
} catch (err) {
tokenCache.tokens = null;
}
}
return tokenCache.tokens;
}

View File

@ -0,0 +1,69 @@
import '@polymer/paper-card/paper-card.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import { enableWrite } from '../common/auth/token_storage.js';
import LocalizeMixin from '../mixins/localize-mixin.js';
import '../resources/ha-style.js';
class HaStoreAuth extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<style include='ha-style'>
paper-card {
position: fixed;
padding: 8px 0;
bottom: 16px;
right: 16px;
}
.card-actions {
text-align: right;
border-top: 0;
margin-right: -4px;
}
:host(.small) paper-card {
bottom: 0;
left: 0;
right: 0;
}
</style>
<paper-card elevation="4">
<div class='card-content'>
[[localize('ui.auth_store.ask')]]
</div>
<div class='card-actions'>
<paper-button on-click='_done'>[[localize('ui.auth_store.decline')]]</paper-button>
<paper-button primary on-click='_save'>[[localize('ui.auth_store.confirm')]]</paper-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
};
}
ready() {
super.ready();
this.classList.toggle('small', window.innerWidth < 600);
}
_save() {
enableWrite();
this._done();
}
_done() {
const card = this.shadowRoot.querySelector('paper-card');
card.style.transition = 'bottom .25s';
card.style.bottom = `-${card.offsetHeight + 8}px`;
setTimeout(() => this.parentNode.removeChild(this), 300);
}
}
customElements.define('ha-store-auth-card', HaStoreAuth);

View File

@ -5,9 +5,10 @@ import {
subscribeEntities,
} from 'home-assistant-js-websocket';
import fetchToken from '../common/auth/fetch_token.js';
import refreshToken_ from '../common/auth/refresh_token.js';
import { redirectLogin, resolveCode, refreshToken } from '../common/auth/token.js';
// import refreshToken_ from '../common/auth/refresh_token.js';
import parseQuery from '../common/util/parse_query.js';
import { loadTokens } from '../common/auth/token_storage.js';
const init = window.createHassConnection = function (password, accessToken) {
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
@ -29,68 +30,11 @@ const init = window.createHassConnection = function (password, accessToken) {
});
};
function clientId() {
return `${location.protocol}//${location.host}/`;
}
function redirectLogin() {
document.location.href = `/auth/authorize?response_type=code&client_id=${encodeURIComponent(clientId())}&redirect_uri=${encodeURIComponent(location.toString())}`;
return new Promise();
}
let tokenCache;
function storeTokens(tokens) {
tokenCache = tokens;
try {
localStorage.tokens = JSON.stringify(tokens);
} catch (err) {} // eslint-disable-line
}
function loadTokens() {
if (tokenCache === undefined) {
try {
const tokens = localStorage.tokens;
tokenCache = tokens ? JSON.parse(tokens) : null;
} catch (err) {
tokenCache = null;
}
}
return tokenCache;
}
window.refreshToken = () => {
const tokens = loadTokens();
if (tokens === null) {
return redirectLogin();
}
return refreshToken_(clientId(), tokens.refresh_token).then((accessTokenResp) => {
const newTokens = Object.assign({}, tokens, accessTokenResp);
storeTokens(newTokens);
return newTokens;
}, () => redirectLogin());
};
function resolveCode(code) {
fetchToken(clientId(), code).then((tokens) => {
storeTokens(tokens);
// Refresh the page and have tokens in place.
document.location.href = 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);
window.hassConnection = resolveCode(query.code).then(newTokens => init(null, newTokens));
return;
}
}
@ -103,14 +47,14 @@ function main() {
if (Date.now() + 30000 > tokens.expires) {
// refresh access token if it will expire in 30 seconds to avoid invalid auth event
window.hassConnection = window.refreshToken().then(newTokens => init(null, newTokens));
window.hassConnection = refreshToken().then(newTokens => init(null, newTokens));
return;
}
window.hassConnection = init(null, tokens).catch((err) => {
if (err !== ERR_INVALID_AUTH) throw err;
return window.refreshToken().then(newTokens => init(null, newTokens));
return refreshToken().then(newTokens => init(null, newTokens));
});
}

View File

@ -1,9 +1,20 @@
import { afterNextRender } from '@polymer/polymer/lib/utils/render-status.js';
import { clearState } from '../../util/ha-pref-storage.js';
import { askWrite } from '../../common/auth/token_storage.js';
export default superClass => class extends superClass {
ready() {
super.ready();
this.addEventListener('hass-logout', () => this._handleLogout());
afterNextRender(null, () => {
if (askWrite()) {
const el = document.createElement('ha-store-auth-card');
this.shadowRoot.appendChild(el);
this.provideHass(el);
import(/* webpackChunkName: "ha-store-auth-card" */ '../../dialogs/ha-store-auth-card.js');
}
});
}
hassConnected() {

View File

@ -9,6 +9,7 @@ import translationMetadata from '../../../build-translations/translationMetadata
import LocalizeMixin from '../../mixins/localize-mixin.js';
import EventsMixin from '../../mixins/events-mixin.js';
import { refreshToken } from '../../common/auth/token.js';
import { getState } from '../../util/ha-pref-storage.js';
import { getActiveTranslation } from '../../util/hass-translation.js';
import hassCallApi from '../../util/hass-call-api.js';
@ -103,7 +104,7 @@ export default superClass =>
try {
// Refresh token if it will expire in 30 seconds
if (auth.accessToken && Date.now() + 30000 > auth.expires) {
const accessToken = await window.refreshToken();
const accessToken = await refreshToken();
conn.options.accessToken = accessToken.access_token;
conn.options.expires = accessToken.expires;
}
@ -112,7 +113,7 @@ export default superClass =>
if (!err || err.status_code !== 401 || !auth.accessToken) throw err;
// If we connect with access token and get 401, refresh token and try again
const accessToken = await window.refreshToken();
const accessToken = await refreshToken();
conn.options.accessToken = accessToken.access_token;
conn.options.expires = accessToken.expires;
return await hassCallApi(host, auth, method, path, parameters);
@ -159,7 +160,7 @@ export default superClass =>
while (this.unsubFuncs.length) {
this.unsubFuncs.pop()();
}
const accessToken = await window.refreshToken();
const accessToken = await refreshToken();
this._handleNewConnProm(window.createHassConnection(null, accessToken));
};

View File

@ -18,6 +18,7 @@ export default superClass => class extends superClass {
provideHass(el) {
this.__provideHass.push(el);
el.hass = this.hass;
}
async _updateHass(obj) {

View File

@ -191,6 +191,12 @@ documentContainer.innerHTML = `<custom-style>
.card-actions ha-call-service-button.warning:not([disabled]) {
color: var(--google-red-500);
}
.card-actions paper-button[primary] {
background-color: var(--primary-color);
color: var(--text-primary-color);
}
</style>
</template>
</dom-module><dom-module id="ha-style-dialog">

View File

@ -326,6 +326,11 @@
}
},
"ui": {
"auth_store": {
"ask": "Do you want to save this login?",
"decline": "No thanks",
"confirm": "Save login"
},
"card": {
"alarm_control_panel": {
"code": "Code",