Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit ddcb7a6

Browse files
authored
Merge pull request #2781 from matrix-org/travis/openid-widget
Widget OpenID reauth implementation
2 parents 7f90607 + 69fcebf commit ddcb7a6

File tree

10 files changed

+282
-7
lines changed

10 files changed

+282
-7
lines changed

res/css/_components.scss

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
@import "./views/dialogs/_ShareDialog.scss";
7171
@import "./views/dialogs/_UnknownDeviceDialog.scss";
7272
@import "./views/dialogs/_UserSettingsDialog.scss";
73+
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
7374
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
7475
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
7576
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
Copyright 2019 Travis Ralston
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_WidgetOpenIDPermissionsDialog .mx_SettingsFlag {
18+
.mx_ToggleSwitch {
19+
display: inline-block;
20+
vertical-align: middle;
21+
margin-right: 8px;
22+
}
23+
24+
.mx_SettingsFlag_label {
25+
display: inline-block;
26+
vertical-align: middle;
27+
}
28+
}

src/FromWidgetPostMessageApi.js

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2018 New Vector Ltd
3+
Copyright 2019 Travis Ralston
34
45
Licensed under the Apache License, Version 2.0 (the 'License');
56
you may not use this file except in compliance with the License.
@@ -20,17 +21,19 @@ import IntegrationManager from './IntegrationManager';
2021
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
2122
import ActiveWidgetStore from './stores/ActiveWidgetStore';
2223

23-
const WIDGET_API_VERSION = '0.0.1'; // Current API version
24+
const WIDGET_API_VERSION = '0.0.2'; // Current API version
2425
const SUPPORTED_WIDGET_API_VERSIONS = [
2526
'0.0.1',
27+
'0.0.2',
2628
];
2729
const INBOUND_API_NAME = 'fromWidget';
2830

29-
// Listen for and handle incomming requests using the 'fromWidget' postMessage
31+
// Listen for and handle incoming requests using the 'fromWidget' postMessage
3032
// API and initiate responses
3133
export default class FromWidgetPostMessageApi {
3234
constructor() {
3335
this.widgetMessagingEndpoints = [];
36+
this.widgetListeners = {}; // {action: func[]}
3437

3538
this.start = this.start.bind(this);
3639
this.stop = this.stop.bind(this);
@@ -45,6 +48,32 @@ export default class FromWidgetPostMessageApi {
4548
window.removeEventListener('message', this.onPostMessage);
4649
}
4750

51+
/**
52+
* Adds a listener for a given action
53+
* @param {string} action The action to listen for.
54+
* @param {Function} callbackFn A callback function to be called when the action is
55+
* encountered. Called with two parameters: the interesting request information and
56+
* the raw event received from the postMessage API. The raw event is meant to be used
57+
* for sendResponse and similar functions.
58+
*/
59+
addListener(action, callbackFn) {
60+
if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
61+
this.widgetListeners[action].push(callbackFn);
62+
}
63+
64+
/**
65+
* Removes a listener for a given action.
66+
* @param {string} action The action that was subscribed to.
67+
* @param {Function} callbackFn The original callback function that was used to subscribe
68+
* to updates.
69+
*/
70+
removeListener(action, callbackFn) {
71+
if (!this.widgetListeners[action]) return;
72+
73+
const idx = this.widgetListeners[action].indexOf(callbackFn);
74+
if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
75+
}
76+
4877
/**
4978
* Register a widget endpoint for trusted postMessage communication
5079
* @param {string} widgetId Unique widget identifier
@@ -117,6 +146,13 @@ export default class FromWidgetPostMessageApi {
117146
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
118147
}
119148

149+
// Call any listeners we have registered
150+
if (this.widgetListeners[event.data.action]) {
151+
for (const fn of this.widgetListeners[event.data.action]) {
152+
fn(event.data, event);
153+
}
154+
}
155+
120156
// Although the requestId is required, we don't use it. We'll be nice and process the message
121157
// if the property is missing, but with a warning for widget developers.
122158
if (!event.data.requestId) {
@@ -164,6 +200,8 @@ export default class FromWidgetPostMessageApi {
164200
if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) {
165201
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
166202
}
203+
} else if (action === 'get_openid') {
204+
// Handled by caller
167205
} else {
168206
console.warn('Widget postMessage event unhandled');
169207
this.sendError(event, {message: 'The postMessage was unhandled'});

src/WidgetMessaging.js

+57-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
Copyright 2017 New Vector Ltd
3+
Copyright 2019 Travis Ralston
34
45
Licensed under the Apache License, Version 2.0 (the "License");
56
you may not use this file except in compliance with the License.
@@ -21,6 +22,11 @@ limitations under the License.
2122

2223
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
2324
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
25+
import Modal from "./Modal";
26+
import MatrixClientPeg from "./MatrixClientPeg";
27+
import SettingsStore from "./settings/SettingsStore";
28+
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
29+
import WidgetUtils from "./utils/WidgetUtils";
2430

2531
if (!global.mxFromWidgetMessaging) {
2632
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
@@ -34,12 +40,14 @@ if (!global.mxToWidgetMessaging) {
3440
const OUTBOUND_API_NAME = 'toWidget';
3541

3642
export default class WidgetMessaging {
37-
constructor(widgetId, widgetUrl, target) {
43+
constructor(widgetId, widgetUrl, isUserWidget, target) {
3844
this.widgetId = widgetId;
3945
this.widgetUrl = widgetUrl;
46+
this.isUserWidget = isUserWidget;
4047
this.target = target;
4148
this.fromWidget = global.mxFromWidgetMessaging;
4249
this.toWidget = global.mxToWidgetMessaging;
50+
this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
4351
this.start();
4452
}
4553

@@ -109,9 +117,57 @@ export default class WidgetMessaging {
109117

110118
start() {
111119
this.fromWidget.addEndpoint(this.widgetId, this.widgetUrl);
120+
this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
112121
}
113122

114123
stop() {
115124
this.fromWidget.removeEndpoint(this.widgetId, this.widgetUrl);
125+
this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
126+
}
127+
128+
async _onOpenIdRequest(ev, rawEv) {
129+
if (ev.widgetId !== this.widgetId) return; // not interesting
130+
131+
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.widgetUrl, this.isUserWidget);
132+
133+
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
134+
if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
135+
this.fromWidget.sendResponse(rawEv, {state: "blocked"});
136+
return;
137+
}
138+
if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
139+
const responseBody = {state: "allowed"};
140+
const credentials = await MatrixClientPeg.get().getOpenIdToken();
141+
Object.assign(responseBody, credentials);
142+
this.fromWidget.sendResponse(rawEv, responseBody);
143+
return;
144+
}
145+
146+
// Confirm that we received the request
147+
this.fromWidget.sendResponse(rawEv, {state: "request"});
148+
149+
// Actually ask for permission to send the user's data
150+
Modal.createTrackedDialog("OpenID widget permissions", '',
151+
WidgetOpenIDPermissionsDialog, {
152+
widgetUrl: this.widgetUrl,
153+
widgetId: this.widgetId,
154+
isUserWidget: this.isUserWidget,
155+
156+
onFinished: async (confirm) => {
157+
const responseBody = {success: confirm};
158+
if (confirm) {
159+
const credentials = await MatrixClientPeg.get().getOpenIdToken();
160+
Object.assign(responseBody, credentials);
161+
}
162+
this.messageToWidget({
163+
api: OUTBOUND_API_NAME,
164+
action: "openid_credentials",
165+
data: responseBody,
166+
}).catch((error) => {
167+
console.error("Failed to send OpenID credentials: ", error);
168+
});
169+
},
170+
},
171+
);
116172
}
117173
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
Copyright 2019 Travis Ralston
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import PropTypes from 'prop-types';
19+
import {_t} from "../../../languageHandler";
20+
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
21+
import sdk from "../../../index";
22+
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
23+
import WidgetUtils from "../../../utils/WidgetUtils";
24+
25+
export default class WidgetOpenIDPermissionsDialog extends React.Component {
26+
static propTypes = {
27+
onFinished: PropTypes.func.isRequired,
28+
widgetUrl: PropTypes.string.isRequired,
29+
widgetId: PropTypes.string.isRequired,
30+
isUserWidget: PropTypes.bool.isRequired,
31+
};
32+
33+
constructor() {
34+
super();
35+
36+
this.state = {
37+
rememberSelection: false,
38+
};
39+
}
40+
41+
_onAllow = () => {
42+
this._onPermissionSelection(true);
43+
};
44+
45+
_onDeny = () => {
46+
this._onPermissionSelection(false);
47+
};
48+
49+
_onPermissionSelection(allowed) {
50+
if (this.state.rememberSelection) {
51+
console.log(`Remembering ${this.props.widgetId} as allowed=${allowed} for OpenID`);
52+
53+
const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
54+
if (!currentValues.allow) currentValues.allow = [];
55+
if (!currentValues.deny) currentValues.deny = [];
56+
57+
const securityKey = WidgetUtils.getWidgetSecurityKey(
58+
this.props.widgetId,
59+
this.props.widgetUrl,
60+
this.props.isUserWidget);
61+
(allowed ? currentValues.allow : currentValues.deny).push(securityKey);
62+
SettingsStore.setValue("widgetOpenIDPermissions", null, SettingLevel.DEVICE, currentValues);
63+
}
64+
65+
this.props.onFinished(allowed);
66+
}
67+
68+
_onRememberSelectionChange = (newVal) => {
69+
this.setState({rememberSelection: newVal});
70+
};
71+
72+
render() {
73+
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
74+
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
75+
76+
return (
77+
<BaseDialog className='mx_WidgetOpenIDPermissionsDialog' hasCancel={true}
78+
onFinished={this.props.onFinished}
79+
title={_t("A widget would like to verify your identity")}>
80+
<div className='mx_WidgetOpenIDPermissionsDialog_content'>
81+
<p>
82+
{_t(
83+
"A widget located at %(widgetUrl)s would like to verify your identity. " +
84+
"By allowing this, the widget will be able to verify your user ID, but not " +
85+
"perform actions as you.", {
86+
widgetUrl: this.props.widgetUrl,
87+
},
88+
)}
89+
</p>
90+
<LabelledToggleSwitch value={this.state.rememberSelection} toggleInFront={true}
91+
onChange={this._onRememberSelectionChange}
92+
label={_t("Remember my selection for this widget")} />
93+
</div>
94+
<DialogButtons
95+
primaryButton={_t("Allow")}
96+
onPrimaryButtonClick={this._onAllow}
97+
cancelButton={_t("Deny")}
98+
onCancel={this._onDeny}
99+
/>
100+
</BaseDialog>
101+
);
102+
}
103+
}

src/components/views/elements/AppTile.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,8 @@ export default class AppTile extends React.Component {
351351
_setupWidgetMessaging() {
352352
// FIXME: There's probably no reason to do this here: it should probably be done entirely
353353
// in ActiveWidgetStore.
354-
const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
354+
const widgetMessaging = new WidgetMessaging(
355+
this.props.id, this.props.url, this.props.userWidget, this.refs.appFrame.contentWindow);
355356
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
356357
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
357358
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);

src/components/views/elements/LabelledToggleSwitch.js

+17-3
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,29 @@ export default class LabelledToggleSwitch extends React.Component {
3131

3232
// Whether or not to disable the toggle switch
3333
disabled: PropTypes.bool,
34+
35+
// True to put the toggle in front of the label
36+
// Default false.
37+
toggleInFront: PropTypes.bool,
3438
};
3539

3640
render() {
3741
// This is a minimal version of a SettingsFlag
42+
43+
let firstPart = <span className="mx_SettingsFlag_label">{this.props.label}</span>;
44+
let secondPart = <ToggleSwitch checked={this.props.value} disabled={this.props.disabled}
45+
onChange={this.props.onChange} />;
46+
47+
if (this.props.toggleInFront) {
48+
const temp = firstPart;
49+
firstPart = secondPart;
50+
secondPart = temp;
51+
}
52+
3853
return (
3954
<div className="mx_SettingsFlag">
40-
<span className="mx_SettingsFlag_label">{this.props.label}</span>
41-
<ToggleSwitch checked={this.props.value} disabled={this.props.disabled}
42-
onChange={this.props.onChange} />
55+
{firstPart}
56+
{secondPart}
4357
</div>
4458
);
4559
}

src/i18n/strings/en_EN.json

+4
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,10 @@
11861186
"Room contains unknown devices": "Room contains unknown devices",
11871187
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.",
11881188
"Unknown devices": "Unknown devices",
1189+
"A widget would like to verify your identity": "A widget would like to verify your identity",
1190+
"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.",
1191+
"Remember my selection for this widget": "Remember my selection for this widget",
1192+
"Deny": "Deny",
11891193
"Unable to load backup status": "Unable to load backup status",
11901194
"Recovery Key Mismatch": "Recovery Key Mismatch",
11911195
"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.",

src/settings/Settings.js

+7
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,13 @@ export const SETTINGS = {
340340
displayName: _td('Show developer tools'),
341341
default: false,
342342
},
343+
"widgetOpenIDPermissions": {
344+
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
345+
default: {
346+
allow: [],
347+
deny: [],
348+
},
349+
},
343350
"RoomList.orderByImportance": {
344351
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
345352
displayName: _td('Order rooms in the room list by most important first instead of most recent'),

0 commit comments

Comments
 (0)