Web3js – Backend-Based Signature Verification: How to Implement?

authenticationSecuritysignatureweb3jweb3js

I'm new in web3 and I'm trying to develop a way to authenticate users in my dapp. I'm used to using the old and safe method with email + password to do that, but recently I realised that I could use signatures instead of my old method.

The idea is simple:

  1. user signs a message and sends it to the backend for verification and authentication
  2. backend decodes it and compares the caller's address with signer's address
  3. backend marks this signature on his side as used and generates auth token to caller
  4. user receives an auth token and has the same access as he could have with the default method e.g. jwt

So, now my frontend uses any web3 wallet and tries to do something like this:

const mess = 'some message user signs'
const signature = await web3.eth.personal.sign(mess, address)
await axios.post(backendUrlAuth, {address: address, signature: signature, signedMessage: mess}, cfg)

Then backend verifies it this way:

public boolean verify(String address, String signature, String signedMessage) throws SignatureException {
    if (address.startsWith("0x")) {
        address = address.substring(2);
    }
    if (signature.startsWith("0x")) {
        signature = signature.substring(2);
    }

    var r = signature.substring(0, 64);
    var s = signature.substring(64, 128);
    var v = signature.substring(128, 130);

    var publicKey = Sign.signedPrefixedMessageToKey(signedMessage.getBytes(),
                    new Sign.SignatureData(
                            Numeric.hexStringToByteArray(v)[0],
                            Numeric.hexStringToByteArray(r),
                            Numeric.hexStringToByteArray(s)))
            .toString(16);

    return Keys.getAddress(publicKey).equals(address);
}

So, I'm here to ask is this way safe or not ?

Best Answer

From my perspective, you should include a wallet nonce to the signature message for a unique clarification. However, I believe it's best practice to use @metamask/eth-sig-util (https://www.npmjs.com/package/@metamask/eth-sig-util) for verify wallet signature. Also, each project has its own way of validating the wallet signature. Here is our method. For FE, I use https://wagmi.sh/react/hooks/useSignMessage for message signing, and for BE, I use metamask/eth-sig-util to do verification.

After user click connect metamask wallet:

  1. FE will call a GET API from BE with query params is the "wallet_address" to get the nonce message.

  2. BE return the nonce message for FE, e.g., "Welcome to ...., Here's a unique message ID: ${user_nonce}"

  3. Then, FE will perform a POST request to BE to verify the user signature using wagmi and axios request as

import { useSignMessage, useWalletClient } from 'wagmi';

export const useEnhanceSignMessage = () => {
  const { signMessageAsync } = useSignMessage();
  const { data } = useWalletClient();

  // signMessageAsync required signer behind the scene, so we need to wait for signer to be ready
  if (data) {
    return { signMessageAsync };
  } else {
    return { signMessageAsync: null };
  }
};


const { signMessageAsync } = useEnhanceSignMessage();

const signature = await signMessageAsync({
       message: nonce as string,
});

res = await axios.post(`/v1/auth`, {
       signature: signature,
       walletAddress: address,
 });

  1. Then, BE receives the signature string message from FE to validate as
const msg = `${defaultConfig.AUTH_MESSAGE.login} ${user.nonce}`;
const msgBufferHex = Buffer.from(msg, 'utf8').toString('hex');
let address = ''
 try {
   address = recoverPersonalSignature({
      data: msgBufferHex,
      signature: signature
   });
 } catch (err) {
    console.log(err);
    throw new UnauthorizedException("Error! Invalid signature");
 }

// Generate token or else.

Hope it helps you.

Related Topic