3

I'm making a dapp that will make calls to a Node.js server. I expect the user to have MetaMask installed and I want to verify that he is the real owner of the current address on MetaMask (i.e. accounts[0]).

This is the user flow I am trying to implement:

  1. User loads up my dapp frontend in his browser.
  2. The browser gets accounts[0] from Web3/MetaMask.
  3. The frontend requests some data specific to accounts[0] from my Node.js API.
  4. On the Node.js server, I need to verify that the request is coming from someone who actually owns the private keys to the address accounts[0]. If this is valid, then I respond with the specific data.

I spent a long time looking at different signing functions with Web3, and I ended up very confused. There is:

  • web3.eth.sign - there's no recover counterpart to this and MetaMask doesn't pop up asking the user to sign something.
  • web3.eth.personal.sign - this one requires a password, I don't want to ask the user to enter his password, shouldn't MetaMask do this?
  • web3.eth.accounts.sign - this seems more like a hashing function than what I need.

I have a feeling that none of the three above functions are what I need. Can someone provide some guidance on how to approach this?

  • I think the solution isn't working anymore. At least I don't get the right public address. Could you please help me finding out, how it has to be done? – Florian Pircher Jul 19 '18 at 18:03
6

I think web3.eth.sign is what you want, but note that it expects a 32-byte string (usually a hash of a message).

This worked for me:

web3.eth.sign(web3.eth.defaultAccount, web3.sha3('test'), function (err, signature) {
  console.log(signature);  // But maybe do some error checking. :-)
});

Then on the server, using ethereumjs-util:

const util = require('ethereumjs-util');
const sig = util.fromRpcSig('<signature from front end>');
const publicKey = util.ecrecover(util.sha3('test'), sig.v, sig.r, sig.s);
const address = util.pubToAddress(publicKey).toString('hex');

You said "MetaMask doesn't pop up asking the user to sign something," but it's supposed to. If it's still not working, please share the code you're using to call web3.eth.sign.

EDIT

Note that in web3.js 1.0, it's web3.util.sha3 instead of web3.sha3.

  • Thanks, I'm going to try this out. If it works, I'll accept your answer. – adrianmc Jan 8 '18 at 4:58
  • this was very helpful, thank you. I had to make two changes to make it work though (as I am using Web3 1.0): (1) web3.utils.sha3 instead of web3.sha3, (2) fromRpcSig(), I needed to pass in the whole thing with the leading 0x. If you add this in an edit, I will accept your answer. – adrianmc Jan 8 '18 at 5:28
  • 1
    Thanks, the leading 0x is indeed needed (regardless of which web3.js version you're using). I edited the answer. – smarx Jan 8 '18 at 5:32
3

The MetaMask's own web3, still 0.2 asof May-2018, sign syntax is --

console.log(web3.version.api);
web3.personal.sign(web3.toHex("message to sign"), accounts[0], 
                   function(err, res) {
    // whatever
});

If you replace MetaMask's web3 with web3.js 1.0.0 (beta), the syntax to use is --

window.web3 = new Web3(web3.currentProvider);
console.log(web3.version);
web3.eth.personal.sign('message to sign', accounts[0])
.then(signature => {
    // whatever
});

In both cases, MetaMask will popup the sign notification

0

I implemented this and it works great. I am using Python/Flask in the backend so you would have to find equivalent backend code for Node:

Backend: store the user by their public address in the database along with a nonce used for signin

The most simple schema for a User/Account is:

public_address = db.Column(db.String(80), primary_key=True, nullable=False, unique=True)
nonce = db.Column(
    INTEGER(unsigned=True),
    nullable=False,
    default=generate_nonce,
)

Where generate nonce is a pseudorandom number generator something like:

def generate_nonce(self, length=8):
    return ''.join([str(randint(0, 9)) for i in range(length)])

Frontend GETs and signs the nonce with web3

Retrieve the public address of the current user:

web3.eth.getAccounts()
        .then((response) => {
            const publicAddressResponse = response[0];

            if (!(typeof publicAddressResponse === "undefined")) {
                setPublicAddress(publicAddressResponse);
                getNonce(publicAddressResponse);
            }
        })
        .catch((e) => {
            console.error(e);
        });

The frontend should do a GET request to retrieve the current nonce for the public address that is trying to sign in. If the account doesn't exist yet then create it and return the nonce anyway:

GET /api/users?publicAddress=${publicAddress}

and then sign the nonce with Metamask:

web3.eth.personal.sign(`I am signing my one-time nonce: ${nonce}`, publicAddress, "test password!")
            .then((signature) => {
                handleAuth(publicAddress, signature)
            });

Frontend then sends the signed nonce to the backend to receive a JWT

Frontend:

axios.post(props.config.serverUrl + '/sessions/', {
            publicAddress: publicAddress,
            signature: signature,
            auth_type: 'ethereum',
        })
            .then((response) => {
                localStorage.setItem('accessToken', response.data.access_token);
            })
            .catch((e) => {
                console.error(e);
            });

Then on the backend you authenticate that the signature came from that public address using web3 libraries and issue a JWT if authenticated. From there it is just normal session management using JWTs, which is not specifically a web3 issue:

@sessions_blueprint.route('/sessions/', methods=['POST'])
def create_session():

    auth_type = request.json.get('auth_type', AuthType.EMAIL)

    public_address = request.json['publicAddress']
    signature = request.json['signature']

    account = EthereumAccount.query.filter_by(public_address=public_address).first()

    if account is None:
        abort(404, 'Public address not registered.')

    original_message = 'I am signing my one-time nonce: {}'.format(account.nonce)
    message_hash = defunct_hash_message(text=original_message)
    signer = w3.eth.account.recoverHash(message_hash, signature=signature)

    if signer == public_address:
        account.nonce = account.generate_nonce()
        db.session.commit()
    else:
        abort(401, 'could not authenticate signature')

    access_token = create_access_token(identity=public_address)
    refresh_token = create_refresh_token(identity=public_address)


    return jsonify({
        'access_token': access_token,
        'refresh_token': refresh_token,
    }), 200

And voila! The user is now authenticated with a JWT on the client side. You can manage sessions and authorisation for routes on the backend however you normally do.

I learnt how to do this from this article: https://www.toptal.com/ethereum/one-click-login-flows-a-metamask-tutorial

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.