From d63c5e7134070a4cd44f36dfb30f94f161370c4f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 13 Mar 2019 00:34:34 -0600 Subject: [PATCH] Basic widget OpenID reauth implementation Covers the minimum of https://github.com/vector-im/riot-web/issues/7153 This does not handling automatically accepting/blocking widgets yet, however. This could lead to dialog irritation. --- src/FromWidgetPostMessageApi.js | 42 ++++++++++++++++++++++++++++-- src/WidgetMessaging.js | 45 +++++++++++++++++++++++++++++++++ src/i18n/strings/en_EN.json | 4 ++- 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index ea7eeba756d..577eabf5ec7 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -1,5 +1,6 @@ /* Copyright 2018 New Vector Ltd +Copyright 2019 Travis Ralston Licensed under the Apache License, Version 2.0 (the 'License'); you may not use this file except in compliance with the License. @@ -20,17 +21,19 @@ import IntegrationManager from './IntegrationManager'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; -const WIDGET_API_VERSION = '0.0.1'; // Current API version +const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ '0.0.1', + '0.0.2', ]; const INBOUND_API_NAME = 'fromWidget'; -// Listen for and handle incomming requests using the 'fromWidget' postMessage +// Listen for and handle incoming requests using the 'fromWidget' postMessage // API and initiate responses export default class FromWidgetPostMessageApi { constructor() { this.widgetMessagingEndpoints = []; + this.widgetListeners = {}; // {action: func[]} this.start = this.start.bind(this); this.stop = this.stop.bind(this); @@ -45,6 +48,32 @@ export default class FromWidgetPostMessageApi { window.removeEventListener('message', this.onPostMessage); } + /** + * Adds a listener for a given action + * @param {string} action The action to listen for. + * @param {Function} callbackFn A callback function to be called when the action is + * encountered. Called with two parameters: the interesting request information and + * the raw event received from the postMessage API. The raw event is meant to be used + * for sendResponse and similar functions. + */ + addListener(action, callbackFn) { + if (!this.widgetListeners[action]) this.widgetListeners[action] = []; + this.widgetListeners[action].push(callbackFn); + } + + /** + * Removes a listener for a given action. + * @param {string} action The action that was subscribed to. + * @param {Function} callbackFn The original callback function that was used to subscribe + * to updates. + */ + removeListener(action, callbackFn) { + if (!this.widgetListeners[action]) return; + + const idx = this.widgetListeners.indexOf(callbackFn); + if (idx !== -1) this.widgetListeners.splice(idx, 1); + } + /** * Register a widget endpoint for trusted postMessage communication * @param {string} widgetId Unique widget identifier @@ -117,6 +146,13 @@ export default class FromWidgetPostMessageApi { return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise } + // Call any listeners we have registered + if (this.widgetListeners[event.data.action]) { + for (const fn of this.widgetListeners[event.data.action]) { + fn(event.data, event); + } + } + // Although the requestId is required, we don't use it. We'll be nice and process the message // if the property is missing, but with a warning for widget developers. if (!event.data.requestId) { @@ -164,6 +200,8 @@ export default class FromWidgetPostMessageApi { if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } + } else if (action === 'get_openid') { + // Handled by caller } else { console.warn('Widget postMessage event unhandled'); this.sendError(event, {message: 'The postMessage was unhandled'}); diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index 5b722df65f9..17ce9360b79 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -1,5 +1,6 @@ /* Copyright 2017 New Vector Ltd +Copyright 2019 Travis Ralston Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,6 +22,10 @@ limitations under the License. import FromWidgetPostMessageApi from './FromWidgetPostMessageApi'; import ToWidgetPostMessageApi from './ToWidgetPostMessageApi'; +import Modal from "./Modal"; +import QuestionDialog from "./components/views/dialogs/QuestionDialog"; +import {_t} from "./languageHandler"; +import MatrixClientPeg from "./MatrixClientPeg"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -40,6 +45,7 @@ export default class WidgetMessaging { this.target = target; this.fromWidget = global.mxFromWidgetMessaging; this.toWidget = global.mxToWidgetMessaging; + this._openIdHandlerRef = this._onOpenIdRequest.bind(this); this.start(); } @@ -109,9 +115,48 @@ export default class WidgetMessaging { start() { this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl); + this.fromWidget.addListener("get_openid", this._openIdHandlerRef); } stop() { this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl); + this.fromWidget.removeListener("get_openid", this._openIdHandlerRef); + } + + _onOpenIdRequest(ev, rawEv) { + if (ev.widgetId !== this.widgetId) return; // not interesting + + // Confirm that we received the request + this.fromWidget.sendResponse(rawEv, {state: "request"}); + + // TODO: Support blacklisting widgets + // TODO: Support whitelisting widgets + + // Actually ask for permission to send the user's data + Modal.createTrackedDialog("OpenID widget permissions", '', QuestionDialog, { + title: _t("A widget would like to verify your identity"), + description: _t( + "A widget located at %(widgetUrl)s would like to verify your identity. " + + "By allowing this, the widget will be able to verify your user ID, but not " + + "perform actions as you.", { + widgetUrl: this.widgetUrl, + }, + ), + button: _t("Allow"), + onFinished: async (confirm) => { + const responseBody = {success: confirm}; + if (confirm) { + const credentials = await MatrixClientPeg.get().getOpenIdToken(); + Object.assign(responseBody, credentials); + } + this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: "openid_credentials", + data: responseBody, + }).catch((error) => { + console.error("Failed to send OpenID credentials: ", error); + }); + }, + }); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8cc85b60368..e13390e2261 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -230,6 +230,9 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", + "A widget would like to verify your identity": "A widget would like to verify your identity", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", + "Allow": "Allow", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -924,7 +927,6 @@ "NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted", "Warning: This widget might use cookies.": "Warning: This widget might use cookies.", "Do you want to load widget from URL:": "Do you want to load widget from URL:", - "Allow": "Allow", "Delete Widget": "Delete Widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?", "Delete widget": "Delete widget",