Authentication in an Ethereum DApp is usually very different from other authentication systems that you find in traditional web apps.
Since all the information stored on the blockchain is completely public, it would make no sense to store passwords, or even hashes of passwords for our user accounts. Instead, a good strategy is to rely on our user's unique Ethereum address.
The well-known DApp "CryptoKitties" does this in a clever way. In order to sign up, you need to sign a message using one of your MetaMask wallet's addresses – proving that you own the corresponding private key. It will then create your account by storing your given user info and address in their database.
The good thing with this is that signing a message is completely free, so you don't need to worry about gas costs from the user's perspective.
In our DApp however, we are storing the user info on the blockchain (more precisely, in the
UserStorage contract), and not in our own private database. Signing a message will therefore not be enough. We will however use the same concept of associating an Ethereum address to a user profile.
Updating our contracts
As you might have noticed from our registration form in the Next.js app, we're collecting some additional information that we're not yet handling in our contracts – the user's first name, last name, bio and their Gravatar email (to display a profile picture).
Let's update our
UserStorage contract to take these new fields into account:
To make our DApp work, we also want to be able to find a profile based on a username or an address, so we'll add two more mappings, each pointing to a user ID:
Now that we've updated the profile structure, we also need to update the
createUser function that's in
UserStorage.sol. Apart from taking into account the new fields, we'll also populate the two new mappings every time a new user is added:
Finally, we'll also update our controller function in
UserController.sol. In it, we first do some checks to make sure that neither the user's address or desired username is taken, before we send all the relevant information to the storage function.
If you try to run
truffle test on this, you'll notice that
Therefore, let's update both our unit and integration tests so that they take these new fields into account.
Re-deploying our contracts
If we had already deployed our contracts to the production environment, the updates made to
UserStorage would pose a problem, since redeploying that contract would clear all the data.
In this case though, since we're still working with our local Ethereum instance, we can simply stop the process running
Ctrl + C, and then relaunch it again, to get a brand new blockchain where the contracts have not been deployed yet. Being able to start from a blank state is sometimes very desirable during the development process!
After restarting your ganache-cli instance, you can redeploy your contracts using:
We'll also run
npm run dev again so that our contract artifacts get recompiled with the new changes we've made.
The only downside with this technique is that your fake MetaMask funds from earlier will be back to zero (since the blockchain is completely reset). You will therefore have to send some funds to it again as we did in the chapter "Using MetaMask".
To do this, run
truffle console in a new terminal window and run the following commands (replacing
YOUR_METAMASK_ADDRESS with your own MetaMask wallet address obviously):
Now your MetaMask wallet should be topped up and ready to go again!
Calling the function with MetaMask
Let's go back to the frontend. We'll first open the file
web3/users.js and edit the
createUser function so that it receives and sends all the parameters needed to create a new profile, and not just the username.
Next, we add the
createUser method in our
RegistrationForm. This method will do some lazy checks to make sure we've filled in all the fields, before it calls the
createUser function above.
With this, you should now be able to fill in the form, make a MetaMask transaction and create your user!
As you can see, there are clear drawbacks to forcing the user to create their profile on the blockchain. Paying $22.73 (although the amount might vary depending on how busy the network is) to create a user is probably not something that many people are willing to do. It's therefore completely understandable why apps like CryptoKitties have taken a more traditional and centralised approach to data storage.
Now that you know how the decentralised version of account creating works, it's up to you to decide exactly what data makes sense to have on the blockchain for your own app! One mix-and-match variant could be to only save the user ID and address on the blockchain, while storing the rest on a good ol' SQL database for example. For the sake of brevity, we won't do that in this course though.
Authenticating the user
So we now have a newly created profile, but from the end-user's perspective, everything still looks the same. The next step in the process it to log in.
No need to log in? How is that possible?
It turns out that your MetaMask account is your login. As we saw before, once you've unlocked your MetaMask wallet, any DApp can see what address you're currently using thanks to
eth.getAccounts(). If they know your address, they know your account.
This is quite different from how web apps usually authenticate users, but it's actually very effective. Imagine no longer having to remember (or generate) unique passwords for every website anymore. Instead, MetaMask is your one and only login. Pretty cool huh?
Now that we know this, let's edit our code so that, as soon as the DApp launches, it checks whether the user's address is connected to any existing user ID in the
Since we want to display the logged-in user's info in the header, we'll do the check in the
Displaying the user info
Finally, we want to display the name and avatar for the logged-in user in our header. So using this ID that we have, we want to fetch the rest of the user's information.
We can do this pretty easily simply by changing our existing
getUserInfo function in
web3/users.js so that it returns all of the fields that we have. The only thing to remember is that we want the info to have some user-friendly formatting, which means converting hex strings to normal strings and
BigNumber to integers.
To make sure it works, we can import the function into our header and log the full user info instead of just the ID:
Alright, we have the info – now we need to display it. We'll start by adding two state variables –
userInfo. If the
loggedIn variable is
true, we'll render a new
Nav component with the user's first name and last name.
As you can see in the
Nav component, we've added a link to a profile page with the username as a query parameter. This page obviously doesn't exist yet, but we'll create it in an upcoming chapter.
Showing an avatar
To top it off, we also want to render the user's profile picture based on the
gravatarEmail that they provided (if no email was provided, we'll just render a default picture).
To make this easy, we install the NPM package gravatar-url.
We then create a new
Avatar component where we simply render the Gravatar image based on an email address.
Finally we import the
Avatar component into our Nav and render it next to the user's name:
You should now have an idea of how to perform authentication checks in Ethereum. The key part to understand that's fundamentally different from traditional web apps, is that your wallet's address is essentially your user account.
In the next chapter, we'll look at how we can let our logged-in user create new tweets through the DApp's interface.