diff --git a/res/css/_components.scss b/res/css/_components.scss
index 4fb0eed4af..f29e30dcb4 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -70,6 +70,7 @@
 @import "./views/dialogs/_ShareDialog.scss";
 @import "./views/dialogs/_UnknownDeviceDialog.scss";
 @import "./views/dialogs/_UserSettingsDialog.scss";
+@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
 @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
 @import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
 @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
diff --git a/res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss b/res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss
new file mode 100644
index 0000000000..a419c105a9
--- /dev/null
+++ b/res/css/views/dialogs/_WidgetOpenIDPermissionsDialog.scss
@@ -0,0 +1,28 @@
+/*
+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.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+.mx_WidgetOpenIDPermissionsDialog .mx_SettingsFlag {
+    .mx_ToggleSwitch {
+        display: inline-block;
+        vertical-align: middle;
+        margin-right: 8px;
+    }
+
+    .mx_SettingsFlag_label {
+        display: inline-block;
+        vertical-align: middle;
+    }
+}
diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
index ea7eeba756..4dd3ea6e6d 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[action].indexOf(callbackFn);
+        if (idx !== -1) this.widgetListeners[action].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 5b722df65f..1d8e1b9cd3 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,11 @@ limitations under the License.
 
 import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
 import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
+import Modal from "./Modal";
+import MatrixClientPeg from "./MatrixClientPeg";
+import SettingsStore from "./settings/SettingsStore";
+import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
+import WidgetUtils from "./utils/WidgetUtils";
 
 if (!global.mxFromWidgetMessaging) {
     global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
@@ -34,12 +40,14 @@ if (!global.mxToWidgetMessaging) {
 const OUTBOUND_API_NAME = 'toWidget';
 
 export default class WidgetMessaging {
-    constructor(widgetId, widgetUrl, target) {
+    constructor(widgetId, widgetUrl, isUserWidget, target) {
         this.widgetId = widgetId;
         this.widgetUrl = widgetUrl;
+        this.isUserWidget = isUserWidget;
         this.target = target;
         this.fromWidget = global.mxFromWidgetMessaging;
         this.toWidget = global.mxToWidgetMessaging;
+        this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
         this.start();
     }
 
@@ -109,9 +117,57 @@ export default class WidgetMessaging {
 
     start() {
         this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl);
+        this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
     }
 
     stop() {
         this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl);
+        this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
+    }
+
+    async _onOpenIdRequest(ev, rawEv) {
+        if (ev.widgetId !== this.widgetId) return; // not interesting
+
+        const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.widgetUrl, this.isUserWidget);
+
+        const settings = SettingsStore.getValue("widgetOpenIDPermissions");
+        if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
+            this.fromWidget.sendResponse(rawEv, {state: "blocked"});
+            return;
+        }
+        if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
+            const responseBody = {state: "allowed"};
+            const credentials = await MatrixClientPeg.get().getOpenIdToken();
+            Object.assign(responseBody, credentials);
+            this.fromWidget.sendResponse(rawEv, responseBody);
+            return;
+        }
+
+        // Confirm that we received the request
+        this.fromWidget.sendResponse(rawEv, {state: "request"});
+
+        // Actually ask for permission to send the user's data
+        Modal.createTrackedDialog("OpenID widget permissions", '',
+            WidgetOpenIDPermissionsDialog, {
+                widgetUrl: this.widgetUrl,
+                widgetId: this.widgetId,
+                isUserWidget: this.isUserWidget,
+
+                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/components/views/dialogs/WidgetOpenIDPermissionsDialog.js b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
new file mode 100644
index 0000000000..62bd1d2521
--- /dev/null
+++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.js
@@ -0,0 +1,103 @@
+/*
+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.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import {_t} from "../../../languageHandler";
+import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
+import sdk from "../../../index";
+import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
+import WidgetUtils from "../../../utils/WidgetUtils";
+
+export default class WidgetOpenIDPermissionsDialog extends React.Component {
+    static propTypes = {
+        onFinished: PropTypes.func.isRequired,
+        widgetUrl: PropTypes.string.isRequired,
+        widgetId: PropTypes.string.isRequired,
+        isUserWidget: PropTypes.bool.isRequired,
+    };
+
+    constructor() {
+        super();
+
+        this.state = {
+            rememberSelection: false,
+        };
+    }
+
+    _onAllow = () => {
+        this._onPermissionSelection(true);
+    };
+
+    _onDeny = () => {
+        this._onPermissionSelection(false);
+    };
+
+    _onPermissionSelection(allowed) {
+        if (this.state.rememberSelection) {
+            console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`);
+
+            const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
+            if (!currentValues.allow) currentValues.allow = [];
+            if (!currentValues.deny) currentValues.deny = [];
+
+            const securityKey = WidgetUtils.getWidgetSecurityKey(
+                this.props.widgetId,
+                this.props.widgetUrl,
+                this.props.isUserWidget);
+            (allowed ? currentValues.allow : currentValues.deny).push(securityKey);
+            SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues);
+        }
+
+        this.props.onFinished(allowed);
+    }
+
+    _onRememberSelectionChange = (newVal) => {
+        this.setState({rememberSelection: newVal});
+    };
+
+    render() {
+        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+        const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+
+        return (
+            <BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true}
+                        onFinished={this.props.onFinished}
+                        title={_t("A widget would like to verify your identity")}>
+                <div className='mx_WidgetOpenIDPermissionsDialog_content'>
+                    <p>
+                        {_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.props.widgetUrl,
+                            },
+                        )}
+                    </p>
+                    <LabelledToggleSwitch value={this.state.rememberSelection} toggleInFront={true}
+                                          onChange={this._onRememberSelectionChange}
+                                          label={_t("Remember my selection for this widget")} />
+                </div>
+                <DialogButtons
+                    primaryButton={_t("Allow")}
+                    onPrimaryButtonClick={this._onAllow}
+                    cancelButton={_t("Deny")}
+                    onCancel={this._onDeny}
+                />
+            </BaseDialog>
+        );
+    }
+}
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 466715dc3c..9444da5be4 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -351,7 +351,8 @@ export default class AppTile extends React.Component {
     _setupWidgetMessaging() {
         // FIXME: There's probably no reason to do this here: it should probably be done entirely
         // in ActiveWidgetStore.
-        const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
+        const widgetMessaging = new WidgetMessaging(
+            this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow);
         ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
         widgetMessaging.getCapabilities().then((requestedCapabilities) => {
             console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.js
index 292c978e88..0cb9b224cf 100644
--- a/src/components/views/elements/LabelledToggleSwitch.js
+++ b/src/components/views/elements/LabelledToggleSwitch.js
@@ -31,15 +31,29 @@ export default class LabelledToggleSwitch extends React.Component {
 
         // Whether or not to disable the toggle switch
         disabled: PropTypes.bool,
+
+        // True to put the toggle in front of the label
+        // Default false.
+        toggleInFront: PropTypes.bool,
     };
 
     render() {
         // This is a minimal version of a SettingsFlag
+
+        let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
+        let secondPart = <ToggleSwitch checked={this.props.value} disabled={this.props.disabled}
+                                         onChange={this.props.onChange} />;
+
+        if (this.props.toggleInFront) {
+            const temp = firstPart;
+            firstPart = secondPart;
+            secondPart = temp;
+        }
+
         return (
             <div className="mx_SettingsFlag">
-                <span className="mx_SettingsFlag_label">{this.props.label}</span>
-                <ToggleSwitch checked={this.props.value} disabled={this.props.disabled}
-                              onChange={this.props.onChange} />
+                {firstPart}
+                {secondPart}
             </div>
         );
     }
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 18bc876966..e85537ba03 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1186,6 +1186,10 @@
     "Room contains unknown devices": "Room contains unknown devices",
     "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
     "Unknown devices": "Unknown devices",
+    "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.",
+    "Remember my selection for this widget": "Remember my selection for this widget",
+    "Deny": "Deny",
     "Unable to load backup status": "Unable to load backup status",
     "Recovery Key Mismatch": "Recovery Key Mismatch",
     "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.": "Backup could not be decrypted with this key: please verify that you entered the correct recovery key.",
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index 4fe53633ff..6e17ffbbd7 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -340,6 +340,13 @@ export const SETTINGS = {
         displayName: _td('Show developer tools'),
         default: false,
     },
+    "widgetOpenIDPermissions": {
+        supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
+        default: {
+            allow: [],
+            deny: [],
+        },
+    },
     "RoomList.orderByImportance": {
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
         displayName: _td('Order rooms in the room list by most important first instead of most recent'),
diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index b5a2ae31fb..41a241c905 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -1,6 +1,7 @@
 /*
 Copyright 2017 Vector Creations Ltd
 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.
@@ -25,6 +26,7 @@ import WidgetEchoStore from '../stores/WidgetEchoStore';
 // before waitFor[Room/User]Widget rejects its promise
 const WIDGET_WAIT_TIME = 20000;
 import SettingsStore from "../settings/SettingsStore";
+import ActiveWidgetStore from "../stores/ActiveWidgetStore";
 
 /**
  * Encodes a URI according to a set of template variables. Variables will be
@@ -396,4 +398,25 @@ export default class WidgetUtils {
 
         return capWhitelist;
     }
+
+    static getWidgetSecurityKey(widgetId, widgetUrl, isUserWidget) {
+        let widgetLocation = ActiveWidgetStore.getRoomId(widgetId);
+
+        if (isUserWidget) {
+            const userWidget = WidgetUtils.getUserWidgetsArray()
+                .find((w) => w.id === widgetId && w.content && w.content.url === widgetUrl);
+
+            if (!userWidget) {
+                throw new Error("No matching user widget to form security key");
+            }
+
+            widgetLocation = userWidget.sender;
+        }
+
+        if (!widgetLocation) {
+            throw new Error("Failed to locate where the widget resides");
+        }
+
+        return encodeURIComponent(`${widgetLocation}::${widgetUrl}`);
+    }
 }