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:
To run the examples used in this article, you’ll need the following:
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.
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.
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).
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.
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:
window.ethereum
, then the user does not have the wallet software installedwindow.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>
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
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.
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 — Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.