diff --git a/backend/boltathon/util/blockstack.py b/backend/boltathon/util/blockstack.py new file mode 100644 index 0000000..f056583 --- /dev/null +++ b/backend/boltathon/util/blockstack.py @@ -0,0 +1,27 @@ +import requests +from jose import jwt +from flask import current_app +from boltathon.util.errors import RequestError + +# Given a Blockstack private key, validate that it was signed by the same +# key as owns a username +def validate_blockstack_auth(id, username, token): + try: + # Match up data with arguments + token = jwt.decode(token, [], options={ 'verify_signature': False }) + if username != token.get('username'): + return False + if id not in token.get('profile_url'): + return False + + # Match up data with Blockstack's atlas node + res = requests.get(f'https://core.blockstack.org/v1/names/{username}') + res = res.json() + if res.get('address') != id: + return False + + return True + except Exception as e: + print(e) + current_app.logger.error(f'Could not lookup username {username} from blockstack.org: {e}') + raise RequestError(code=500, message='Failed to lookup Blockstack identity') diff --git a/backend/boltathon/views/api.py b/backend/boltathon/views/api.py index 83789f1..7d7d445 100644 --- a/backend/boltathon/views/api.py +++ b/backend/boltathon/views/api.py @@ -8,6 +8,7 @@ from boltathon.util.node import get_pubkey_from_credentials, make_invoice, lookup_invoice from boltathon.util.errors import RequestError from boltathon.util.mail import send_email_once +from boltathon.util.blockstack import validate_blockstack_auth from boltathon.models.user import User, self_user_schema, public_user_schema, public_users_schema from boltathon.models.connection import Connection, public_connections_schema from boltathon.models.tip import Tip, tip_schema, tips_schema @@ -164,10 +165,14 @@ def get_tip(tip_id): @use_args({ 'id': fields.Str(required=True), 'username': fields.Str(required=True), + 'token': fields.Str(required=True), }) def blockstack_auth(args): - # TODO: Server-side verification - # Find or create a new user and add the connection + # Assert that they generated a valid token + if not validate_blockstack_auth(args.get('id'), args.get('username'), args.get('token')): + raise RequestError(code=400, message='Invalid Blockstack token provided') + + # Find or create a new user and add the connection user = Connection.get_user_by_connection( site='blockstack', site_id=args.get('id'), diff --git a/backend/requirements.txt b/backend/requirements.txt index ed7fd39..3013539 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,3 +19,6 @@ bitstring==3.1.5 webargs==5.2.0 flask-cors==3.0.7 sendgrid==6.0.4 + +# JWT package that supports ES256K for blockstack +git+git://github.com/pohutukawa/python-jose@28cd302d#egg=python-jose diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 88cc876..7c17d06 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -93,6 +93,7 @@ class API { return this.request('POST', '/auth/blockstack', { id: data.identityAddress, username: data.username, + token: data.authResponseToken, }); } diff --git a/frontend/src/pages/BlockstackAuth.tsx b/frontend/src/pages/BlockstackAuth.tsx index 2d7d9ff..104014a 100644 --- a/frontend/src/pages/BlockstackAuth.tsx +++ b/frontend/src/pages/BlockstackAuth.tsx @@ -1,20 +1,39 @@ import React from 'react'; -import { Loader } from 'semantic-ui-react'; +import { Loader, Message } from 'semantic-ui-react'; import { withRouter, RouteComponentProps } from 'react-router'; import * as blockstack from 'blockstack.js'; import { UserData } from 'blockstack.js/lib/auth/authApp'; -import api, { User } from '../api'; +import api from '../api'; + +interface State { + error: string | null; +} + +class BlockstackAuth extends React.Component { + state: State = { + error: null, + }; -class BlockstackAuth extends React.Component { componentDidMount() { if (blockstack.isUserSignedIn()) { this.auth(blockstack.loadUserData()); } else if (blockstack.isSignInPending()) { - blockstack.handlePendingSignIn().then(this.auth) + blockstack.handlePendingSignIn().then(this.auth); } } render() { + const { error } = this.state; + + if (error) { + return ( + + Failed to authenticate with Blockstack + {error} + + ); + } + return Connecting to Blockstack...; } @@ -23,9 +42,9 @@ class BlockstackAuth extends React.Component { api.blockstackAuth(data).then(res => { history.replace('/user/me'); }).catch(err => { - alert(err.message); + this.setState({ error: err.message }); }); }; }; -export default withRouter(BlockstackAuth); \ No newline at end of file +export default withRouter(BlockstackAuth);