Blocksism Labs Logo

4 minutes read

User authentication via Metamask & Portis

User authentication via external blockchain wallets such as MetaMask, Portis. Instead of using the traditional auth method where you enter a username and password.

Michał Mirończuk
11/08/2020 12:00 AM

Introduction 💬

Applications that use blockchain wallets often face a problem with user authentication. Instead of using the traditional authentication method where the user has to provide a username and password, the user can use external wallets such as MetaMask or Portis. They provide the low level features of each wallet the ability to sign a specific message that will help us recognize and verify the user. In this post, I will walk you through a simple solution and give you a basic user authentication implementation the above wallets and ethereum cryptographic libraries together with JSON Web Token (JWT). If you are not familiar with JWT, please visit this website.

In general, the whole process consists of 6 steps:

Client

  1. Establishing a message on which we will work
  2. Signing message by the user (using private key implicitly)
  3. Sending signature and user address to the server

Server

  1. Verifying signed message
  2. Generating JWT
  3. Sending access token to the user

Diagram 📌

As I am an advocate of presenting knowledge in a graphical way, I have prepared the sequence diagram below, which will help us in better understanding of the dataflow between parties.

Diagram Image

Implementation 🔥

Because of big interested in NestJS, I have used this framework for backend side. Progressive framework facilities building efficient, scalable Node.js server-side applications and provides many embedded modules like @nestjs/jwt package. For client application I have used React as the most popular solution for building frontend side.

The entire implementation of this demo has been uploaded to the GitHub repository on my profile here.

Client 📍

Major part of client code - signing the transaction is in the LoginComponent in authenticate method

authenticate = async () => {
  const accountAddress = this.props.account;
  const signature = await this.props.web3.currentProvider.send('personal_sign', [
    getMessage(),
    accountAddress, //from which account should be signed. Web3, metamask will sign message by private key inconspicuously.
  ]);
  const signatureResult = signature.result;
  const requestOptions = {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ accountAddress, signature: signatureResult }),
  };
  //make request to local server
  return fetch('http://localhost:3001/auth/login', requestOptions)
    .then((response) => response.json())
    .then((token) => {
      this.setState({ access_token: token });
    });
};

Thanks to extended web3 library we have an access to many functions. To sign a message we have took adventage of personal_sign method where we need to pass two arguments:

  1. message - data which we are signing, has to be the same and common for the client app as well as for the server
  2. account address - from which account should be signed. Web3 library will sign message by private key implicitly. 💡

Isn't it easy to generate a signature? 🚀🚀🚀

Simple GIF

Now we are able to send the signature together with wallet address to our server 😄

Let's move on to the back-end side where the verification operations will be performed on the secure side.

Server 📍

If we look at the diagram, we are reminded that the backend has to recover the address and compare it with the one sent. Therefore, our application service comes into play with the code below.

@Injectable()
export class AppService {
  constructor(private readonly jwtService: JwtService) {}

  loginUser(loginDto: LoginDto): string {
    const { accountAddress, signature } = loginDto;
    var recoveredAddr;
    try {
      recoveredAddr = recoverPersonalSignature({
        data: getMessage(),
        sig: signature,
      });

    } catch (err) {
      throw new HttpException('Problem with signature verification.', 403);
    }

    if (recoveredAddr.toLowerCase() !== accountAddress.toLowerCase()) {
      throw new HttpException('Signature is not correct.', 400);
    }
    //save your user here (i.e var user = await this.usersService.createWalletAccountIfNotExist(createUserDto);)
    const payload: JwtUser = {
      account_address: accountAddress,
    };

    const access_token = this.jwtService.sign(payload);
    return access_token;
  }
}

On input, the service takes the wallet address and signature. To recover the address, the service must use the function called recoverPersonalSignature provided by eth-sig-util library. The function also requires a shared message but this time the signature is passed instead of the address. ✔️

In the next step is to compare the recovered address with the address received from the user. If they are not equal, an exception is thrown. ✔️

Finally JwtService is injected into AppService in order to generate new access token for the user. ✔️

Different approach? 🤔

A slightly more advanced and secure solution may be that the server generates an initial message that will require the user to sign this particular message.

Tags:
Development

Michał Mirończuk

Blockchain Developer

Experienced Blockchain/Full Stack Developer with a 6-year tenure in the crypto realm.

Member since Mar 15, 2019

Latest Posts

03 Jan 20213 minutes read
Solidity: Upgradable Contracts, Tokens
Michał Mirończuk
Michał Mirończuk
Michał Mirończuk
View All

Get Free
Quote

hire@blocksism.com

Sign Up For Newsletter

Receive 50% discount on first project