EIP-712 signatures for login & signup (#189)
* Signup requires valid EIP-712 signature. Refactor some auth reducer nomenclature for consistency. * Add auth endpoint for logging in that checks for valid signature, like create user. * Fix tests, move dummy data into test_data.py. * No strict slashes.
This commit is contained in:
parent
d8eba48847
commit
250d5fb7a9
|
@ -10,6 +10,7 @@ from grant.extensions import bcrypt, migrate, db, ma, mail
|
|||
def create_app(config_object="grant.settings"):
|
||||
app = Flask(__name__.split(".")[0])
|
||||
app.config.from_object(config_object)
|
||||
app.url_map.strict_slashes = False
|
||||
register_extensions(app)
|
||||
register_blueprints(app)
|
||||
register_shellcontext(app)
|
||||
|
|
|
@ -4,7 +4,7 @@ from flask_yoloapi import endpoint, parameter
|
|||
|
||||
from .models import User, SocialMedia, Avatar, users_schema, user_schema, db
|
||||
from grant.proposal.models import Proposal, proposal_team
|
||||
from grant.utils.auth import requires_sm
|
||||
from grant.utils.auth import requires_sm, verify_signed_auth, BadSignatureException
|
||||
|
||||
blueprint = Blueprint('user', __name__, url_prefix='/api/v1/users')
|
||||
|
||||
|
@ -50,12 +50,34 @@ def get_user(user_identity):
|
|||
parameter('emailAddress', type=str, required=True),
|
||||
parameter('displayName', type=str, required=True),
|
||||
parameter('title', type=str, required=True),
|
||||
parameter('signedMessage', type=str, required=True),
|
||||
parameter('rawTypedData', type=str, required=True)
|
||||
)
|
||||
def create_user(account_address, email_address, display_name, title):
|
||||
def create_user(
|
||||
account_address,
|
||||
email_address,
|
||||
display_name,
|
||||
title,
|
||||
signed_message,
|
||||
raw_typed_data
|
||||
):
|
||||
existing_user = User.get_by_email_or_account_address(email_address=email_address, account_address=account_address)
|
||||
if existing_user:
|
||||
return {"message": "User with that address or email already exists"}, 409
|
||||
|
||||
# Handle signature
|
||||
try:
|
||||
sig_address = verify_signed_auth(signed_message, raw_typed_data)
|
||||
if sig_address.lower() != account_address.lower():
|
||||
return {
|
||||
"message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format(
|
||||
sig_address=sig_address,
|
||||
account_address=account_address
|
||||
)
|
||||
}, 400
|
||||
except BadSignatureException:
|
||||
return {"message": "Invalid message signature"}, 400
|
||||
|
||||
# TODO: Handle avatar & social stuff too
|
||||
user = User.create(
|
||||
account_address=account_address,
|
||||
|
@ -66,6 +88,30 @@ def create_user(account_address, email_address, display_name, title):
|
|||
result = user_schema.dump(user)
|
||||
return result
|
||||
|
||||
@blueprint.route("/auth", methods=["POST"])
|
||||
@endpoint.api(
|
||||
parameter('accountAddress', type=str, required=True),
|
||||
parameter('signedMessage', type=str, required=True),
|
||||
parameter('rawTypedData', type=str, required=True)
|
||||
)
|
||||
def auth_user(account_address, signed_message, raw_typed_data):
|
||||
existing_user = User.get_by_email_or_account_address(account_address=account_address)
|
||||
if not existing_user:
|
||||
return {"message": "No user exists with that address"}, 400
|
||||
|
||||
try:
|
||||
sig_address = verify_signed_auth(signed_message, raw_typed_data)
|
||||
if sig_address.lower() != account_address.lower():
|
||||
return {
|
||||
"message": "Message signature address ({sig_address}) doesn't match account_address ({account_address})".format(
|
||||
sig_address=sig_address,
|
||||
account_address=account_address
|
||||
)
|
||||
}, 400
|
||||
except BadSignatureException:
|
||||
return {"message": "Invalid message signature"}, 400
|
||||
|
||||
return user_schema.dump(existing_user)
|
||||
|
||||
@blueprint.route("/<user_identity>", methods=["PUT"])
|
||||
@endpoint.api(
|
||||
|
|
|
@ -31,6 +31,25 @@ def verify_token(token):
|
|||
return data
|
||||
|
||||
|
||||
# Custom exception for bad auth
|
||||
class BadSignatureException(Exception):
|
||||
pass
|
||||
|
||||
def verify_signed_auth(signature, typed_data):
|
||||
loaded_typed_data = ast.literal_eval(typed_data)
|
||||
url = AUTH_URL + "/message/recover"
|
||||
payload = json.dumps({"sig": signature, "data": loaded_typed_data})
|
||||
headers = {'content-type': 'application/json'}
|
||||
response = requests.request("POST", url, data=payload, headers=headers)
|
||||
json_response = response.json()
|
||||
recovered_address = json_response.get('recoveredAddress')
|
||||
|
||||
if not recovered_address:
|
||||
raise BadSignatureException("Authorization signature is invalid")
|
||||
|
||||
return recovered_address
|
||||
|
||||
|
||||
def requires_auth(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
|
@ -50,24 +69,19 @@ def requires_auth(f):
|
|||
def requires_sm(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
typed_data = request.headers.get('RawTypedData', None)
|
||||
signature = request.headers.get('MsgSignature', None)
|
||||
typed_data = request.headers.get('RawTypedData', None)
|
||||
|
||||
if typed_data and signature:
|
||||
loaded_typed_data = ast.literal_eval(typed_data)
|
||||
url = AUTH_URL + "/message/recover"
|
||||
payload = json.dumps({"sig": signature, "data": loaded_typed_data})
|
||||
headers = {'content-type': 'application/json'}
|
||||
response = requests.request("POST", url, data=payload, headers=headers)
|
||||
json_response = response.json()
|
||||
recovered_address = json_response.get('recoveredAddress')
|
||||
auth_address = None
|
||||
try:
|
||||
auth_address = verify_signed_auth(signature, typed_data)
|
||||
except BadSignatureException:
|
||||
return jsonify(message="Invalid auth message signature"), 401
|
||||
|
||||
if not recovered_address:
|
||||
return jsonify(message="No user exists with address: {}".format(recovered_address)), 401
|
||||
|
||||
user = User.get_by_email_or_account_address(account_address=recovered_address)
|
||||
user = User.get_by_email_or_account_address(account_address=auth_address)
|
||||
if not user:
|
||||
return jsonify(message="No user exists with address: {}".format(recovered_address)), 401
|
||||
return jsonify(message="No user exists with address: {}".format(auth_address)), 401
|
||||
|
||||
g.current_user = user
|
||||
return f(*args, **kwargs)
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
import json
|
||||
import random
|
||||
from grant.proposal.models import CATEGORIES
|
||||
|
||||
message = {
|
||||
"sig": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c",
|
||||
"data": {
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "version", "type": "string"},
|
||||
{"name": "chainId", "type": "uint256"},
|
||||
{"name": "verifyingContract", "type": "address"}
|
||||
],
|
||||
"Person": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "wallet", "type": "address"}
|
||||
],
|
||||
"Mail": [
|
||||
{"name": "from", "type": "Person"},
|
||||
{"name": "to", "type": "Person"},
|
||||
{"name": "contents", "type": "string"}
|
||||
]
|
||||
},
|
||||
"primaryType": "Mail",
|
||||
"domain": {
|
||||
"name": "Ether Mail",
|
||||
"version": "1",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
|
||||
},
|
||||
"message": {
|
||||
"from": {
|
||||
"name": "Cow",
|
||||
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
|
||||
},
|
||||
"to": {
|
||||
"name": "Bob",
|
||||
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
|
||||
},
|
||||
"contents": "Hello, Bob!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user = {
|
||||
"accountAddress": '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826',
|
||||
"displayName": 'Groot',
|
||||
"emailAddress": 'iam@groot.com',
|
||||
"title": 'I am Groot!',
|
||||
"avatar": {
|
||||
"link": 'https://avatars2.githubusercontent.com/u/1393943?s=400&v=4'
|
||||
},
|
||||
"socialMedias": [
|
||||
{
|
||||
"link": 'https://github.com/groot'
|
||||
}
|
||||
],
|
||||
"signedMessage": message["sig"],
|
||||
"rawTypedData": json.dumps(message["data"])
|
||||
}
|
||||
|
||||
team = [user]
|
||||
|
||||
milestones = [
|
||||
{
|
||||
"title": "All the money straightaway",
|
||||
"description": "cool stuff with it",
|
||||
"date": "June 2019",
|
||||
"payoutPercent": "100",
|
||||
"immediatePayout": False
|
||||
}
|
||||
]
|
||||
|
||||
proposal = {
|
||||
"team": team,
|
||||
"crowdFundContractAddress": "0x20000",
|
||||
"content": "## My Proposal",
|
||||
"title": "Give Me Money",
|
||||
"milestones": milestones,
|
||||
"category": random.choice(CATEGORIES)
|
||||
}
|
|
@ -1,39 +1,7 @@
|
|||
import json
|
||||
|
||||
from ..config import BaseTestConfig
|
||||
|
||||
account_address = '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826'
|
||||
|
||||
message = {
|
||||
"sig": "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c",
|
||||
"data": {"types": {"EIP712Domain": [{"name": "name", "type": "string"}, {"name": "version", "type": "string"},
|
||||
{"name": "chainId", "type": "uint256"},
|
||||
{"name": "verifyingContract", "type": "address"}],
|
||||
"Person": [{"name": "name", "type": "string"}, {"name": "wallet", "type": "address"}],
|
||||
"Mail": [{"name": "from", "type": "Person"}, {"name": "to", "type": "Person"},
|
||||
{"name": "contents", "type": "string"}]}, "primaryType": "Mail",
|
||||
"domain": {"name": "Ether Mail", "version": "1", "chainId": 1,
|
||||
"verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},
|
||||
"message": {"from": {"name": "Cow", "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},
|
||||
"to": {"name": "Bob", "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},
|
||||
"contents": "Hello, Bob!"}}
|
||||
|
||||
}
|
||||
|
||||
user = {
|
||||
"accountAddress": account_address,
|
||||
"displayName": 'Groot',
|
||||
"emailAddress": 'iam@groot.com',
|
||||
"title": 'I am Groot!',
|
||||
"avatar": {
|
||||
"link": 'https://avatars2.githubusercontent.com/u/1393943?s=400&v=4'
|
||||
},
|
||||
"socialMedias": [
|
||||
{
|
||||
"link": 'https://github.com/groot'
|
||||
}
|
||||
]
|
||||
}
|
||||
from ..test_data import user, message
|
||||
|
||||
|
||||
class TestRequiredSignedMessageDecorator(BaseTestConfig):
|
||||
|
|
|
@ -1,49 +1,12 @@
|
|||
import copy
|
||||
import json
|
||||
import random
|
||||
|
||||
from grant.proposal.models import CATEGORIES
|
||||
from grant.proposal.models import Proposal
|
||||
from grant.user.models import User
|
||||
from ..config import BaseTestConfig
|
||||
from ..test_data import team, proposal
|
||||
from mock import patch
|
||||
|
||||
milestones = [
|
||||
{
|
||||
"title": "All the money straightaway",
|
||||
"description": "cool stuff with it",
|
||||
"date": "June 2019",
|
||||
"payoutPercent": "100",
|
||||
"immediatePayout": False
|
||||
}
|
||||
]
|
||||
|
||||
team = [
|
||||
{
|
||||
"accountAddress": "0x1",
|
||||
"displayName": 'Groot',
|
||||
"emailAddress": 'iam@groot.com',
|
||||
"title": 'I am Groot!',
|
||||
"avatar": {
|
||||
"link": 'https://avatars2.githubusercontent.com/u/1393943?s=400&v=4'
|
||||
},
|
||||
"socialMedias": [
|
||||
{
|
||||
"link": 'https://github.com/groot'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
proposal = {
|
||||
"team": team,
|
||||
"crowdFundContractAddress": "0x20000",
|
||||
"content": "## My Proposal",
|
||||
"title": "Give Me Money",
|
||||
"milestones": milestones,
|
||||
"category": random.choice(CATEGORIES)
|
||||
}
|
||||
|
||||
|
||||
class TestAPI(BaseTestConfig):
|
||||
def test_create_new_user_via_proposal_by_account_address(self):
|
||||
|
|
|
@ -63,9 +63,21 @@ export function createUser(payload: {
|
|||
emailAddress: string;
|
||||
displayName: string;
|
||||
title: string;
|
||||
token: string;
|
||||
signedMessage: string;
|
||||
rawTypedData: string;
|
||||
}): Promise<{ data: TeamMember }> {
|
||||
return axios.post(`/api/v1/users/`, payload).then(res => {
|
||||
return axios.post('/api/v1/users', payload).then(res => {
|
||||
res.data = formatTeamMemberFromGet(res.data);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
export function authUser(payload: {
|
||||
accountAddress: string;
|
||||
signedMessage: string;
|
||||
rawTypedData: string;
|
||||
}): Promise<{ data: TeamMember }> {
|
||||
return axios.post('/api/v1/users/auth', payload).then(res => {
|
||||
res.data = formatTeamMemberFromGet(res.data);
|
||||
return res;
|
||||
});
|
||||
|
|
|
@ -5,4 +5,23 @@ const instance = axios.create({
|
|||
headers: {},
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
// Do nothing to responses
|
||||
res => res,
|
||||
// Try to parse error message if possible
|
||||
err => {
|
||||
if (err.response && err.response.data) {
|
||||
// Our backend's handled error responses
|
||||
if (err.response.data.message) {
|
||||
err.message = err.response.data.message;
|
||||
}
|
||||
// Some flask middlewares return error data like this
|
||||
if (err.response.data.data) {
|
||||
err.message = err.response.data.data;
|
||||
}
|
||||
}
|
||||
return Promise.reject(err);
|
||||
},
|
||||
);
|
||||
|
||||
export default instance;
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
@max-width: 460px;
|
||||
|
||||
.SignIn {
|
||||
&-container {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
max-width: @max-width;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 1px 2px rgba(#000, 0.2);
|
||||
|
@ -44,4 +46,9 @@
|
|||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-error {
|
||||
max-width: @max-width;
|
||||
margin: 1rem auto 0;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from 'antd';
|
||||
import { Button, Alert } from 'antd';
|
||||
import { authActions } from 'modules/auth';
|
||||
import { TeamMember } from 'types';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
@ -29,7 +29,7 @@ type Props = StateProps & DispatchProps & OwnProps;
|
|||
|
||||
class SignIn extends React.Component<Props> {
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const { user, authUserError } = this.props;
|
||||
return (
|
||||
<div className="SignIn">
|
||||
<div className="SignIn-container">
|
||||
|
@ -48,6 +48,16 @@ class SignIn extends React.Component<Props> {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
{authUserError && (
|
||||
<Alert
|
||||
className="SignIn-error"
|
||||
type="error"
|
||||
message="Failed to sign in"
|
||||
description={authUserError}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Temporarily only supporting web3, so there are no other identites
|
||||
<p className="SignIn-back">
|
||||
|
|
|
@ -3,34 +3,46 @@ import { Dispatch } from 'redux';
|
|||
import { sleep } from 'utils/helpers';
|
||||
import { generateAuthSignatureData } from 'utils/auth';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { createUser as apiCreateUser, getUser as apiGetUser } from 'api/api';
|
||||
import {
|
||||
createUser as apiCreateUser,
|
||||
getUser as apiGetUser,
|
||||
authUser as apiAuthUser,
|
||||
} from 'api/api';
|
||||
import { signData } from 'modules/web3/actions';
|
||||
import { AuthSignatureData } from 'types';
|
||||
|
||||
type GetState = () => AppState;
|
||||
|
||||
const getAuthToken = (address: string, dispatch: Dispatch<any>) => {
|
||||
const getAuthSignature = (
|
||||
address: string,
|
||||
dispatch: Dispatch<any>,
|
||||
): Promise<AuthSignatureData> => {
|
||||
const sigData = generateAuthSignatureData(address);
|
||||
return (dispatch(
|
||||
signData(sigData.data, sigData.types, sigData.primaryType),
|
||||
) as any) as string;
|
||||
) as any) as Promise<AuthSignatureData>;
|
||||
};
|
||||
|
||||
// Auth from previous state
|
||||
export function authUser(address: string, signature?: Falsy | string) {
|
||||
// Auth from previous state, or request signature with new auth
|
||||
export function authUser(address: string, authSignature?: Falsy | AuthSignatureData) {
|
||||
return async (dispatch: Dispatch<any>) => {
|
||||
dispatch({ type: types.AUTH_USER_PENDING });
|
||||
|
||||
try {
|
||||
const res = await apiGetUser(address);
|
||||
if (!signature) {
|
||||
signature = await getAuthToken(address, dispatch);
|
||||
if (!authSignature) {
|
||||
authSignature = await getAuthSignature(address, dispatch);
|
||||
}
|
||||
const res = await apiAuthUser({
|
||||
accountAddress: address,
|
||||
signedMessage: authSignature.signedMessage,
|
||||
rawTypedData: JSON.stringify(authSignature.rawTypedData),
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: types.AUTH_USER_FULFILLED,
|
||||
payload: {
|
||||
user: res.data,
|
||||
token: signature,
|
||||
authSignature,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
|
@ -53,19 +65,20 @@ export function createUser(user: {
|
|||
dispatch({ type: types.CREATE_USER_PENDING });
|
||||
|
||||
try {
|
||||
const token = await getAuthToken(user.address, dispatch);
|
||||
const authSignature = await getAuthSignature(user.address, dispatch);
|
||||
const res = await apiCreateUser({
|
||||
accountAddress: user.address,
|
||||
emailAddress: user.email,
|
||||
displayName: user.name,
|
||||
title: user.title,
|
||||
token,
|
||||
signedMessage: authSignature.signedMessage,
|
||||
rawTypedData: JSON.stringify(authSignature.rawTypedData),
|
||||
});
|
||||
dispatch({
|
||||
type: types.CREATE_USER_FULFILLED,
|
||||
payload: {
|
||||
user: res.data,
|
||||
token,
|
||||
authSignature,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
|
|
|
@ -5,5 +5,5 @@ export const authPersistConfig: PersistConfig = {
|
|||
key: 'auth',
|
||||
storage,
|
||||
version: 1,
|
||||
whitelist: ['token', 'tokenAddress'],
|
||||
whitelist: ['authSignature', 'authSignatureAddress'],
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import types from './types';
|
||||
// TODO: Use a common User type instead of this
|
||||
import { TeamMember } from 'types';
|
||||
import { TeamMember, AuthSignatureData } from 'types';
|
||||
|
||||
export interface AuthState {
|
||||
user: TeamMember | null;
|
||||
|
@ -13,10 +13,10 @@ export interface AuthState {
|
|||
isCreatingUser: boolean;
|
||||
createUserError: string | null;
|
||||
|
||||
token: string | null;
|
||||
tokenAddress: string | null;
|
||||
isSigningToken: boolean;
|
||||
signTokenError: string | null;
|
||||
authSignature: AuthSignatureData | null;
|
||||
authSignatureAddress: string | null;
|
||||
isSigningAuth: boolean;
|
||||
signAuthError: string | null;
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: AuthState = {
|
||||
|
@ -30,13 +30,16 @@ export const INITIAL_STATE: AuthState = {
|
|||
checkedUsers: {},
|
||||
isCheckingUser: false,
|
||||
|
||||
token: null,
|
||||
tokenAddress: null,
|
||||
isSigningToken: false,
|
||||
signTokenError: null,
|
||||
authSignature: null,
|
||||
authSignatureAddress: null,
|
||||
isSigningAuth: false,
|
||||
signAuthError: null,
|
||||
};
|
||||
|
||||
export default function createReducer(state: AuthState = INITIAL_STATE, action: any) {
|
||||
export default function createReducer(
|
||||
state: AuthState = INITIAL_STATE,
|
||||
action: any,
|
||||
): AuthState {
|
||||
switch (action.type) {
|
||||
case types.AUTH_USER_PENDING:
|
||||
return {
|
||||
|
@ -49,8 +52,8 @@ export default function createReducer(state: AuthState = INITIAL_STATE, action:
|
|||
return {
|
||||
...state,
|
||||
user: action.payload.user,
|
||||
token: action.payload.token, // TODO: Make this the real token
|
||||
tokenAddress: action.payload.user.ethAddress,
|
||||
authSignature: action.payload.authSignature, // TODO: Make this the real token
|
||||
authSignatureAddress: action.payload.user.ethAddress,
|
||||
isAuthingUser: false,
|
||||
};
|
||||
case types.AUTH_USER_REJECTED:
|
||||
|
@ -70,8 +73,8 @@ export default function createReducer(state: AuthState = INITIAL_STATE, action:
|
|||
return {
|
||||
...state,
|
||||
user: action.payload.user,
|
||||
token: action.payload.token,
|
||||
tokenAddress: action.payload.user.ethAddress,
|
||||
authSignature: action.payload.authSignature,
|
||||
authSignatureAddress: action.payload.user.ethAddress,
|
||||
isCreatingUser: false,
|
||||
checkedUsers: {
|
||||
...state.checkedUsers,
|
||||
|
@ -113,30 +116,30 @@ export default function createReducer(state: AuthState = INITIAL_STATE, action:
|
|||
case types.SIGN_TOKEN_PENDING:
|
||||
return {
|
||||
...state,
|
||||
token: null,
|
||||
isSigningToken: true,
|
||||
signTokenError: null,
|
||||
authSignature: null,
|
||||
isSigningAuth: true,
|
||||
signAuthError: null,
|
||||
};
|
||||
case types.SIGN_TOKEN_FULFILLED:
|
||||
return {
|
||||
...state,
|
||||
token: action.payload.token,
|
||||
tokenAddress: action.payload.address,
|
||||
isSigningToken: false,
|
||||
authSignature: action.payload.authSignature,
|
||||
authSignatureAddress: action.payload.address,
|
||||
isSigningAuth: false,
|
||||
};
|
||||
case types.SIGN_TOKEN_REJECTED:
|
||||
return {
|
||||
...state,
|
||||
isSigningToken: false,
|
||||
signTokenError: action.payload,
|
||||
isSigningAuth: false,
|
||||
signAuthError: action.payload,
|
||||
};
|
||||
|
||||
case types.LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
token: null,
|
||||
tokenAddress: null,
|
||||
authSignature: null,
|
||||
authSignatureAddress: null,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { SagaIterator } from 'redux-saga';
|
||||
import { select, put, all, takeEvery } from 'redux-saga/effects';
|
||||
import { REHYDRATE } from 'redux-persist';
|
||||
import { getAuthTokenAddress, getAuthToken } from './selectors';
|
||||
import { getAuthSignature, getAuthSignatureAddress } from './selectors';
|
||||
import { authUser } from './actions';
|
||||
|
||||
export function* authFromToken(): SagaIterator {
|
||||
const address: ReturnType<typeof getAuthTokenAddress> = yield select(
|
||||
getAuthTokenAddress,
|
||||
const address: ReturnType<typeof getAuthSignatureAddress> = yield select(
|
||||
getAuthSignatureAddress,
|
||||
);
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
const signature: ReturnType<typeof getAuthToken> = yield select(getAuthToken);
|
||||
const signature: ReturnType<typeof getAuthSignature> = yield select(getAuthSignature);
|
||||
|
||||
// TODO: Figure out how to type redux-saga with thunks
|
||||
yield put<any>(authUser(address, signature));
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AppState as S } from 'store/reducers';
|
||||
|
||||
export const getAuthToken = (s: S) => s.auth.token;
|
||||
export const getAuthTokenAddress = (s: S) => s.auth.tokenAddress;
|
||||
export const getAuthSignature = (s: S) => s.auth.authSignature;
|
||||
export const getAuthSignatureAddress = (s: S) => s.auth.authSignatureAddress;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { fetchProposal, fetchProposals } from 'modules/proposals/actions';
|
|||
import { PROPOSAL_CATEGORY } from 'api/constants';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Wei } from 'utils/units';
|
||||
import { TeamMember } from 'types';
|
||||
import { TeamMember, AuthSignatureData } from 'types';
|
||||
|
||||
type GetState = () => AppState;
|
||||
|
||||
|
@ -412,17 +412,14 @@ export function withdrawRefund(crowdFundContract: any, address: string) {
|
|||
};
|
||||
}
|
||||
|
||||
// TODO: Fill me out with all param types.
|
||||
// TODO: _ will be primaryType for EIP-712
|
||||
export function signData(data: any, dataTypes: any, _: string) {
|
||||
// TODO: Fill out params with typed data
|
||||
export function signData(data: object, dataTypes: object, primaryType: string) {
|
||||
return async (dispatch: Dispatch<any>, getState: GetState) => {
|
||||
dispatch({ type: types.SIGN_DATA_PENDING });
|
||||
const state = getState();
|
||||
const { web3, accounts } = state.web3;
|
||||
// Needed for EIP-712
|
||||
// const chainId = await web3.eth.net.getId();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const handleErr = (err: any) => {
|
||||
console.error(err);
|
||||
dispatch({
|
||||
|
@ -438,28 +435,30 @@ export function signData(data: any, dataTypes: any, _: string) {
|
|||
throw new Error('No web3 instance available!');
|
||||
}
|
||||
|
||||
// TODO: This typing is hella broken
|
||||
const chainId = await web3.eth.net.getId();
|
||||
const rawTypedData = {
|
||||
domain: {
|
||||
name: 'Grant.io',
|
||||
version: 1,
|
||||
chainId,
|
||||
},
|
||||
types: {
|
||||
...dataTypes,
|
||||
EIP712Domain: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'version', type: 'string' },
|
||||
{ name: 'chainId', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
message: data,
|
||||
primaryType,
|
||||
};
|
||||
|
||||
(web3.currentProvider as any).sendAsync(
|
||||
{
|
||||
method: 'eth_signTypedData',
|
||||
params: [
|
||||
Object.keys(dataTypes).map(k => ({
|
||||
...dataTypes[k],
|
||||
value: data[k],
|
||||
})),
|
||||
accounts[0],
|
||||
],
|
||||
// EIP-712
|
||||
// params: [JSON.stringify({
|
||||
// primaryType,
|
||||
// domain: {
|
||||
// origin: window.location.origin,
|
||||
// version: 1,
|
||||
// chainId,
|
||||
// },
|
||||
// types: dataTypes,
|
||||
// message: data,
|
||||
// }), accounts[0]],
|
||||
method: 'eth_signTypedData_v3',
|
||||
params: [accounts[0], JSON.stringify(rawTypedData)],
|
||||
from: accounts[0],
|
||||
},
|
||||
(err: Error | undefined, res: any) => {
|
||||
if (err) {
|
||||
|
@ -469,8 +468,12 @@ export function signData(data: any, dataTypes: any, _: string) {
|
|||
const msg = web3ErrorToString(res.error);
|
||||
return handleErr(new Error(msg));
|
||||
}
|
||||
dispatch({ type: types.SIGN_DATA_FULFILLED, payload: res.result });
|
||||
resolve(res.result);
|
||||
const payload: AuthSignatureData = {
|
||||
signedMessage: res.result,
|
||||
rawTypedData,
|
||||
};
|
||||
dispatch({ type: types.SIGN_DATA_FULFILLED, payload });
|
||||
resolve(payload);
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
|
|
|
@ -40,15 +40,17 @@ export function generateAuthSignatureData(address: string) {
|
|||
return {
|
||||
data: { message, time },
|
||||
types: {
|
||||
message: {
|
||||
name: 'Message Proof',
|
||||
type: 'string',
|
||||
},
|
||||
time: {
|
||||
name: 'Time',
|
||||
type: 'string',
|
||||
},
|
||||
authorization: [
|
||||
{
|
||||
name: 'Message Proof',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'Time',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
primaryType: 'message',
|
||||
primaryType: 'authorization',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export interface AuthSignatureData {
|
||||
signedMessage: string;
|
||||
rawTypedData: any;
|
||||
}
|
|
@ -1,43 +1,8 @@
|
|||
import { User, TeamMember } from './user';
|
||||
import { SocialAccountMap, SOCIAL_TYPE, SocialInfo } from './social';
|
||||
import { CreateFormState } from './create';
|
||||
import { Comment, UserComment } from './comment';
|
||||
import {
|
||||
MILESTONE_STATE,
|
||||
Milestone,
|
||||
ProposalMilestone,
|
||||
CreateMilestone,
|
||||
} from './milestone';
|
||||
import { Update } from './update';
|
||||
import {
|
||||
Contributor,
|
||||
CrowdFund,
|
||||
Proposal,
|
||||
ProposalWithCrowdFund,
|
||||
ProposalComments,
|
||||
ProposalUpdates,
|
||||
UserProposal,
|
||||
} from './proposal';
|
||||
|
||||
export {
|
||||
User,
|
||||
UserComment,
|
||||
UserProposal,
|
||||
TeamMember,
|
||||
SocialAccountMap,
|
||||
SOCIAL_TYPE,
|
||||
SocialInfo,
|
||||
CreateFormState,
|
||||
CreateMilestone,
|
||||
Contributor,
|
||||
MILESTONE_STATE,
|
||||
Milestone,
|
||||
ProposalMilestone,
|
||||
CrowdFund,
|
||||
Proposal,
|
||||
ProposalWithCrowdFund,
|
||||
Comment,
|
||||
ProposalComments,
|
||||
Update,
|
||||
ProposalUpdates,
|
||||
};
|
||||
export * from './user';
|
||||
export * from './social';
|
||||
export * from './create';
|
||||
export * from './comment';
|
||||
export * from './milestone';
|
||||
export * from './update';
|
||||
export * from './proposal';
|
||||
export * from './api';
|
||||
|
|
Loading…
Reference in New Issue