Ori Pomerantz Ori has been securing IT, and teaching others to do the same, since the start of his career in 1995. In addition to raising seven kids with his wife, he works @optimismPBC and writes about Ethereum on the side.

Improve user authentication with Web3 wallets

7 min read 2065

Web3 Logo

Abstract

This article will demonstrate how a software wallet, a standard piece of Web3 technology, can transform complex user registration and authentication into a trivial procedure.

The term “wallet” is used in Ethereum in two distinct ways. One is the software that provides access to Ethereum, such as MetaMask or Rabby. The other is the passphrase from which a user account is derived. Each account is a combination of a secret private key and a public key, which is also the account address.

In this article, we’ll cover the following:

Prerequisites

To run the examples used in this article, you’ll need the following:

  • Git installed
  • Node.js installed
  • Yarn installed
  • A wallet installed on your browser. If you do not already have a Web3 wallet, you can install one from MetaMask by following the directions on its site

For the examples used in this article, I use AngularJS to manage the JavaScript. You do not need to be an Angular expert to understand the code, I’ll explain any pertinent details along the way.

Challenges of user registration and authentication

There’s no easy way to say it – registering and authenticating users can be a pain. Users routinely forget their passwords, lose access to their sign on email address, and ask friends or family members to help them guess answers to their password reset questions. However, in Web3, a user’s browser is more than just a window to the internet, it also contains DeFi tools, such as a wallet. This is a game changer, because these wallets can provide web applications with strong user identification.

Users have a big incentive to keep their Web3 wallet secure, because it is associated with assets. If another party gains control of a user’s wallet (for example, by using their recovery phrase), they could steal its assets. Similarly, if a user loses their wallet access or recovery information, any assets associated with the wallet would effectively be lost (or locked away) forever.

Importance of keeping keys secure

When you generate a new key, you get a list of code words (typically twelve, but it could be fifteen, eighteen, or more). This list is your key. It is crucial to keep it safe, usually with a password in your software wallet on your computer and a paper copy of the list located elsewhere (I keep mine in a safe).

  • If you lose your key, you will no longer have any access to assets or accounts that the key protects
  • If someone else has access to your key, that person is now you for all purposes. There is no way for applications, or the blockchain itself, to distinguish between you and another person who has your key

Wallets do not expire. You do not need to change your word list periodically. But if you think it might have been disclosed, it is critical to create a new one and transfer everything you can to the new wallet.

Client-side user identification

It is standard for Ethereum wallets to expose a global API on a web browser in window.ethereum. At this point we’re only concerned with two items relating to the API:

We made a custom demo for .
No really. Click here to check it out.

  1. Whether it exists at all. If there is no window.ethereum, then the user does not have the wallet software installed
  2. The call window.ethereum.request({method: 'eth_requestAccounts'}). This call instructs the wallet to ask users for permission to identify themselves (as one or more addresses)

Let’s take a look at how we can get user information using client-side code:

Angular uses a variable called $scope to hold the state of the application. This is the code that initializes it:

myApp.controller("myCtrl", function($scope, $http) {

The .addr and .error fields hold the information we get from the wallet. The first is the address in case the user agrees to identify to us. The second is the error if anything goes wrong (for example, if the user rejects the request to identify).

    $scope.addr = "" 
    $scope.error = ""

The below function checks for the presence of a wallet by verifying if window.ethereum is defined. For security reasons, if the HTML is read from a local file rather than a server, then window.ethereum will be undefined. To debug this, even the UI, we’ll need to serve it from somewhere.

    $scope.isWallet = () => typeof window.ethereum !== 'undefined'

This next function attempts to connect us to the wallet. We request that the wallet inform us of the available accounts using the eth_requestAccounts method. The window.ethereum.request function returns a Promise object.

    $scope.connect = () => {
        window.ethereum.request({method: 'eth_requestAccounts'})

If the call is successful, we’ll get a list of addresses that the user allows us to see. If we want just a single address for identity, we can take the first address in the list. In addition, we’ll want to clear any errors that may exist from previous interactions.

        .then(arr => {
            $scope.addr = arr[0]
            $scope.error = ""

When scope variables are updated from within a Promise, AngularJS sometimes doesn’t see that they have been updated. Calling $scope.$digest() tells it to process the new values:

            $scope.$digest()

In the following code we specify that only the error message should be displayed if there is an error. Displaying the entire error with the stack trace would be confusing to users.

        },    // success
        err => {
            $scope.error = err.message
            $scope.$digest()

Now, let’s take a look at how Angular handles conditional views. The ng-if attribute specifies a condition, and the HTML tag is only shown if the condition is true. In the below code, we check that error is not a false value, such as the empty string.

<div ng-if="error">

This is in contrast to the below JavaScript code, where $scope. is added automatically to the variable names:

        })   // error
    }    // $scope.connect

In Angular we display values in HTML using {{<expression>}}.

    <h2>Error: {{error}}</h2>

In the below code, we display the address for any instance where both a wallet and an address exist. Later in this article, we’ll demonstrate how to send this information to a server safely.

</div>
<div ng-if="isWallet()">
    <h2 ng-if="addr>Address: {{addr}}</h2>

The ng-click attribute specifies the JavaScript code to be executed when the button is clicked. Below, the JavaScript code is embedded in our HTML:

    <button ng-if="!addr" ng-click="connect()">
    Connect to Ethereum
    </button>

If the address is empty, we ask the user to install MetaMask or another Ethereum wallet in order to connect:

</div>    <!-- ng-if="isWallet()" -->
<div ng-if="!isWallet()">
    To use this application, please install 
    <a href="https://metamask.io">MetaMask</a> or
    some other Ethereum wallet.
</div>

Server-side user identification

Having user identity in the browser is nice, but usually we want the server to be able to identify the user. Relying on code to share user identity can be dangerous, due to the risk of false user identity being provided by nefarious apps.

A safer solution is to have the user sign a message with the session ID. This requires the user to have the private key that corresponds to the address. Only the legitimate user would be in possession of this key.

Let’s follow the below steps to see a server-side user identification example in action:

Download the source code repository from GitHub:

    git clone https://github.com/qbzzt/qbzzt.github.io/ \ 
    demos

Download the code packages:

    cd demos/LogRocket/20220228-user-identity
    yarn

Run the server:

    node server.js

Navigate to http://localhost:8000 in the browser and select Server side user authentication.

Here, we’ll be given the option to identify ourselves. To test the security, try three options. First, provide an actual address. Now, try using a fake address. Next, try entering a random signature.

The new functionality is mostly in the $scope.sign function, so let’s take a closer look:

$scope.sign = async addr => {
    const httpResult = await $scope.$http.get("/session")

In this tutorial, we’re using the Express.js web server and express-session middleware for session management. Since this package does not let JavaScript read the session ID, we’ll use the HTTP client inside Angular, $http.

Let’s go to the server code to see the response to this request:

app.get("/session", (req, res) => {

The above code shows how to handle an HTTP GET request in Express. The path is /session, and the function being called has two parameters: the request object (req) and the response object (res).

Next, the following function reads the session ID from the request (req.sessionID) and then sends it as the response (res.send):

  res.send(req.sessionID)
})

Back on the browser, httpResult contains not just the data sent by the server, but also the header fields. We only want the data:

    $scope.sessionId = httpResult.data

Now, we’ll call on the wallet using the personal_sign method. This method requires three parameters. The first parameter is the string being signed. The second parameter is the address that signs (the one associated with the wallet has the private key). The third parameter is a password (in this case, the empty string).

    $scope.walletResp = await window.ethereum.request(
    {
        method: "personal_sign",
        params: [`My session ID: ${$scope.sessionId}`,
                          $scope.addr, ""]
    })

We’ll send the server two parameters: the signed response and our address. Let’s see how the server code processes those values:

    const sigUrl = `/signature?sig=${$scope.walletResp}&addr=${addr}`
    $scope.authResult = (await $scope.$http.get(sigUrl)).data

The personal_sign method is actually a constant prefix that is added to data so that it can not imitate or mimic a transaction. This prevents potential abuse.

app.get("/signature", (req, res) => {
  let error = "", realAddr = ""
  const expectedMsg = `My session ID: ${req.sessionID}`
  const hash = ethers.utils.id(
`\x19Ethereum Signed Message:\n${expectedMsg.length}${expectedMsg}`)

The query parameters, addr and sig in this case, are available under req.query:

  const claimedAddr = req.query.addr

We verify a signature in Ethereum by trying to recover the address that created the signature:

  try {
    realAddr = ethers.utils.recoverAddress(hash, req.query.sig)

In the case of a random signature, it’s possible either to get a random address or an error. We need to cover both cases.

  } catch (err) {
    error = err.reason
  }

The ethers package gives us a checksum address, one in which the case of the letter digits (a-f) is a checksum on the address. However, MetaMask gives us an address that is all lowercase. To check if the two addresses are identical, we convert all the letters to lowercase. This step is important, because there may be other Web3 wallets that provide addresses in all uppercase, for example, and we want to work with those wallets too.

  if (error)  {
    res.send(`ERROR: ${error}`)
  } else {
    if (realAddr.toLowerCase() === claimedAddr.toLowerCase())

Back in the client, the response is stored in $scope.authResult. This is displayed to the user once we are connected to Ethereum:

      res.send(`Legitimate, welcome ${realAddr}`)
    else
      res.send(`Fraud!!! You are not ${claimedAddr}, you are ${realAddr}!`)
  } // if (error) else

})    // app.get("signature")

The function $scope.invalidSignature() is similar to $scope.sign(), except that instead of creating a legitimate signature, it sends out 130 random bytes. As signatures come in from any source, whether trusted or untrusted, it is important to handle them correctly.

    $scope.$digest()
}    // $scope.sign

Conclusion

Software wallets make user authentication trivial. The wallet handles everything for us; we simply need to verify a signature. There are no credentials to manage or recovery questions to track.

In this article, we discussed the challenges associated with user registration and identification and the importance of keeping keys secure. We also walked through a tutorial on both client-side and server-side user identification.

Now that you’ve seen how easy it is to handle user authentication with wallets, I hope you’ll use wallets in your web applications and encourage users to install them. Web3 wallets improve user authentication and make everyone’s life a lot simpler.

The client-side user identification and server-side user identification examples referenced in this article are available on GitHub.

WazirX, Bitso, and Coinsquare use LogRocket to proactively monitor their Web3 apps

Client-side issues that impact users’ ability to activate and transact in your apps can drastically affect your bottom line. If you’re interested in monitoring UX issues, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — .

Ori Pomerantz Ori has been securing IT, and teaching others to do the same, since the start of his career in 1995. In addition to raising seven kids with his wife, he works @optimismPBC and writes about Ethereum on the side.

Leave a Reply