Secure API access via single-use tokens (#5892)

* Single use token for dapp permissions

* Add accountsInfo & allAccountsInfo

* Covert token -> dappName in requests
This commit is contained in:
Jaco Greeff 2017-06-21 15:15:23 +02:00 committed by GitHub
parent 97c67bc259
commit a6d3d4ea4c
10 changed files with 54 additions and 21 deletions

2
js/package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "parity.js", "name": "parity.js",
"version": "1.7.91", "version": "1.7.92",
"lockfileVersion": 1, "lockfileVersion": 1,
"dependencies": { "dependencies": {
"@parity/abi": { "@parity/abi": {

View File

@ -76,10 +76,10 @@ export default class DappsMethods extends Component {
key={ `${dappIndex}_${requestIndex}` } key={ `${dappIndex}_${requestIndex}` }
> >
<MethodCheck <MethodCheck
checked={ methodsStore.permissions[`${method}:${id}`] || false } checked={ methodsStore.hasAppPermission(method, id) }
dappId={ id } dappId={ id }
method={ method } method={ method }
onToggle={ methodsStore.toggleMethodPermission } onToggle={ methodsStore.toggleAppPermission }
/> />
</td> </td>
)) ))

View File

@ -16,12 +16,15 @@
import { action, observable } from 'mobx'; import { action, observable } from 'mobx';
import { sha3 } from '@parity/api/util/sha3';
import filteredRequests from '../DappRequests/filteredRequests'; import filteredRequests from '../DappRequests/filteredRequests';
export default class Store { export default class Store {
@observable filteredRequests = Object.keys(filteredRequests); @observable filteredRequests = Object.keys(filteredRequests);
@observable modalOpen = false; @observable modalOpen = false;
@observable permissions = {}; @observable permissions = {};
@observable tokens = {};
@action closeModal = () => { @action closeModal = () => {
this.modalOpen = false; this.modalOpen = false;
@ -31,22 +34,40 @@ export default class Store {
this.modalOpen = true; this.modalOpen = true;
} }
@action addMethodPermission = (method, token) => { @action createToken = (appId) => {
const id = `${method}:${token}`; const token = sha3(`${appId}:${Date.now()}`);
this.tokens = Object.assign({}, this.tokens, {
[token]: appId
});
return token;
}
@action addTokenPermission = (method, token) => {
const id = `${method}:${this.tokens[token]}`;
this.permissions = Object.assign({}, this.permissions, { this.permissions = Object.assign({}, this.permissions, {
[id]: true [id]: true
}); });
} }
@action toggleMethodPermission = (method, token) => { @action toggleAppPermission = (method, appId) => {
const id = `${method}:${token}`; const id = `${method}:${appId}`;
this.permissions = Object.assign({}, this.permissions, { this.permissions = Object.assign({}, this.permissions, {
[id]: !this.permissions[id] [id]: !this.permissions[id]
}); });
} }
hasTokenPermission = (method, token) => {
return this.hasAppPermission(method, this.tokens[token]);
}
hasAppPermission = (method, appId) => {
return this.permissions[`${method}:${appId}`] || false;
}
static instance = null; static instance = null;
static get () { static get () {

View File

@ -19,18 +19,22 @@ import { FormattedMessage } from 'react-intl';
import { Button } from '@parity/ui'; import { Button } from '@parity/ui';
export default function Request ({ className, approveRequest, denyRequest, queueId, request: { from, method } }) { import DappsStore from '../../Dapps/dappsStore';
export default function Request ({ appId, className, approveRequest, denyRequest, queueId, request: { from, method } }) {
const _onApprove = () => approveRequest(queueId, false); const _onApprove = () => approveRequest(queueId, false);
const _onApproveAll = () => approveRequest(queueId, true); const _onApproveAll = () => approveRequest(queueId, true);
const _onReject = () => denyRequest(queueId); const _onReject = () => denyRequest(queueId);
const app = DappsStore.get().getAppById(appId);
return ( return (
<div className={ className }> <div className={ className }>
<FormattedMessage <FormattedMessage
id='dappRequests.request.info' id='dappRequests.request.info'
defaultMessage='Received request for {method} from {from}' defaultMessage='Received request for {method} from {appName}'
values={ { values={ {
from, appName: app.name,
method method
} } } }
/> />
@ -66,6 +70,7 @@ export default function Request ({ className, approveRequest, denyRequest, queue
} }
Request.propTypes = { Request.propTypes = {
appId: PropTypes.string.isRequired,
className: PropTypes.string, className: PropTypes.string,
approveRequest: PropTypes.func.isRequired, approveRequest: PropTypes.func.isRequired,
denyRequest: PropTypes.func.isRequired, denyRequest: PropTypes.func.isRequired,

View File

@ -31,8 +31,9 @@ function DappRequests () {
return ( return (
<div className={ styles.requests }> <div className={ styles.requests }>
{ {
store.squashedRequests.map(({ queueId, request: { data } }) => ( store.squashedRequests.map(({ appId, queueId, request: { data } }) => (
<Request <Request
appId={ appId }
className={ styles.request } className={ styles.request }
approveRequest={ store.approveRequest } approveRequest={ store.approveRequest }
denyRequest={ store.rejectRequest } denyRequest={ store.rejectRequest }

View File

@ -15,5 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default { export default {
'parity_accountsInfo': {},
'parity_allAccountsInfo': {},
'parity_hashContent': {} 'parity_hashContent': {}
}; };

View File

@ -55,9 +55,10 @@ export default class Store {
} }
@action queueRequest = (request) => { @action queueRequest = (request) => {
const appId = this.methodsStore.tokens[request.data.from];
let queueId = ++nextQueueId; let queueId = ++nextQueueId;
this.requests = this.requests.concat([{ queueId, request }]); this.requests = this.requests.concat([{ appId, queueId, request }]);
} }
@action approveSingleRequest = ({ queueId, request: { data, source } }) => { @action approveSingleRequest = ({ queueId, request: { data, source } }) => {
@ -72,8 +73,7 @@ export default class Store {
const { request: { data: { method, token } } } = queued; const { request: { data: { method, token } } } = queued;
const requests = this.findMatchingRequests(method, token); const requests = this.findMatchingRequests(method, token);
// TODO: Use single-use token, map back to app name this.methodsStore.addTokenPermission(method, token);
this.methodsStore.addMethodPermission(method, token);
requests.forEach(this.approveSingleRequest); requests.forEach(this.approveSingleRequest);
} else { } else {
this.approveSingleRequest(queued); this.approveSingleRequest(queued);
@ -122,10 +122,7 @@ export default class Store {
return; return;
} }
const filterId = `${method}:${token}`; if (filteredRequests[method] && !this.methodsStore.hasTokenPermission(method, token)) {
// TODO: Use single-use token, map back to app name
if (filteredRequests[method] && !this.methodsStore.permissions[filterId]) {
this.queueRequest({ data, origin, source }); this.queueRequest({ data, origin, source });
return; return;
} }

View File

@ -291,6 +291,10 @@ export default class DappsStore extends EventEmitter {
this.setDisplayApps(visibility); this.setDisplayApps(visibility);
}); });
} }
getAppById = (id) => {
return this.apps.find((app) => app.id === id);
}
} }
export { export {

View File

@ -42,6 +42,7 @@ import SecureApi from '~/secureApi';
import Application from './Application'; import Application from './Application';
import Dapp from './Dapp'; import Dapp from './Dapp';
import { setupProviderFilters } from './DappRequests'; import { setupProviderFilters } from './DappRequests';
import DappMethodsStore from './DappMethods/store';
import Dapps from './Dapps'; import Dapps from './Dapps';
injectTapEventPlugin(); injectTapEventPlugin();
@ -76,7 +77,9 @@ const dapps = [].concat(viewsDapps, builtinDapps);
const dappsHistory = HistoryStore.get('dapps'); const dappsHistory = HistoryStore.get('dapps');
function onEnterDapp ({ params: { id } }) { function onEnterDapp ({ params: { id } }) {
window.web3Provider = new Api.Provider.PostMessage(id, window); const token = DappMethodsStore.get().createToken(id);
window.web3Provider = new Api.Provider.PostMessage(token, window);
if (!dapps[id] || !dapps[id].skipHistory) { if (!dapps[id] || !dapps[id].skipHistory) {
dappsHistory.add(id); dappsHistory.add(id);

View File

@ -25,8 +25,8 @@ export const AttachFileIcon = (props) => <Icon name='attach' { ...props } />;
export const BackgroundIcon = (props) => <Icon name='image' { ...props } />; export const BackgroundIcon = (props) => <Icon name='image' { ...props } />;
export const CancelIcon = (props) => <Icon name='cancel' { ...props } />; export const CancelIcon = (props) => <Icon name='cancel' { ...props } />;
export const CheckIcon = (props) => <Icon name='check' { ...props } />; export const CheckIcon = (props) => <Icon name='check' { ...props } />;
export const CheckboxTickedIcon = (props) => <Icon name='check circle outline' { ...props } />; export const CheckboxTickedIcon = (props) => <Icon name='checkmark box' { ...props } />;
export const CheckboxUntickedIcon = (props) => <Icon name='radio' { ...props } />; export const CheckboxUntickedIcon = (props) => <Icon name='square outline' { ...props } />;
export const CloseIcon = (props) => <Icon name='close' { ...props } />; export const CloseIcon = (props) => <Icon name='close' { ...props } />;
export const CompareIcon = (props) => <Icon name='exchange' { ...props } />; export const CompareIcon = (props) => <Icon name='exchange' { ...props } />;
export const ComputerIcon = (props) => <Icon name='desktop' { ...props } />; export const ComputerIcon = (props) => <Icon name='desktop' { ...props } />;