On My Blockchain Why is the Continue Button Not Available
Online users are becoming increasingly resistant to traditional email/password registration processes. One-click social login functionality via Facebook, Google, or GitHub turns out to be a much more desirable alternative. However, it comes with a trade-off.
Pros of social media login integration:
- No more cumbersome form-filling.
- No need to remember yet another username/password pair.
- The whole process takes seconds instead of minutes.
Cons of social media login integration:
- Since the user's information is loaded from external providers, this raises a huge privacy concern on how providers use all this personal data. For example, at the time of writing, Facebook is facing data privacy issues.
This article introduces a new login method to blockchain development: A one-click, cryptographically-secure login flow using the MetaMask extension, with all data stored on our own back end. We call it: "Login with MetaMask".
A picture being worth a thousand words, here is a demo of the login flow we are going to build:
Looks good? Let's get started!
The basic idea is that it's cryptographically easy to prove the ownership of an account by signing a piece of data using a private key. If you manage to sign a precise piece of data generated by our back end, then the back end will consider you the owner of that public address. Therefore, we can build a message-signing-based authentication mechanism with a user's public address as their identifier.
If it doesn't seem clear, that's all right, because we'll explain it bit-by-bit:
- The MetaMask Browser Extension
- How the Login Flow Works
- Why the Login Flow Works
- Let's Build It Together
- It's Production-ready Today
- Shortcomings on Mobile
Please note that while we will be using tools connected to the Ethereum blockchain (MetaMask, Ethereum public addresses), this login process does not actually need the blockchain: It only needs its cryptography functions. That being said, with MetaMask becoming such a popular extension, now seems a good time to introduce this login flow.
If you already know what MetaMask is, feel free to skip this section.
MetaMask is a browser plugin, available as the MetaMask Chrome extension or Firefox Add-on. At its core, it serves as an Ethereum wallet: By installing it, you will get access to a unique Ethereum public address, with which you can start sending and receiving ether or tokens.
But MetaMask does something more than an Ethereum wallet. As a browser extension, it can interact with the current webpage you're browsing. It does so by injecting a JavaScript library called web3.js in every webpage you visit. Once injected, a web3
object will be available via window.web3
in the JavaScript code of this website. To have a look at what this object looks like, just type window.web3
in the Chrome or Firefox DevTools console, if you have MetaMask installed.
Web3.js is a JavaScript interface to the Ethereum blockchain. There are functions to:
- Get the latest block of the chain (
web3.eth.getBlockNumber
) - Check the current active account on MetaMask (
web3.eth.coinbase
) - Get the balance of any account (
web3.eth.getBalance
) - Send transactions (
web3.eth.sendTransaction
) - Sign messages with the private key of the current account (
web3.personal.sign
) - …and much more
When MetaMask is installed, any front-end code can get access to all these functions, and interact with the blockchain. They are called dapps or DApps (for decentralized apps–sometimes even styled "ĐApps").
Most functions in web3.js are read functions (get block, get balance, etc.), and web3
will give the response immediately. However, some functions (like web3.eth.sendTransaction
and web3.personal.sign
) need the current account to sign some data with its private key. These functions trigger MetaMask to show a confirmation screen, to double-check that the user knows what she or he is signing.
Let's see how to use MetaMask for this. To make a simple test, paste the following line in the DevTools console:
web3.personal.sign(web3.fromUtf8("Hello from Toptal!"), web3.eth.coinbase, console.log);
This command means: Sign my message, converted from utf8 to hex, with the coinbase account (i.e. current account), and as a callback, print the signature. A MetaMask popup will appear, and if you sign it, the signed message will be printed.
We will be using web3.personal.sign
in our login flow.
A final note about this section: MetaMask injects web3.js into your current browser, but there are actually other standalone browsers which also inject web3.js, like Mist, for example. However, in my opinion, MetaMask offers today the best UX and simplest transition for regular users to explore dapps.
How the Login Flow Works
Let's start with the how. The how will hopefully convince you that it's secure, so I'll keep the why part short.
As stated in the overview, we will forget about the blockchain. We have a traditional Web 2.0 client-server RESTful architecture. We will make one assumption: That all users visiting our front-end web page have MetaMask installed. With this assumption, we will show how a passwordless cryptographically-secure login flow works.
Step 1: Modify the User Model (Back-end)
First of all, our User
model needs to have two new required fields: publicAddress
and nonce
. Additionally, publicAddress
needs to be unique. You can keep the usual username
, email
, and password
fields—especially if you want to implement your MetaMask login parallely to an email/password login—but they are optional.
The signup process will also slightly differ, as publicAddress
will be a required field on signup, if the user wishes to use a MetaMask login. Rest assured, the user will never need to type their publicAddress
manually, since it can be fetched via web3.eth.coinbase
.
Step 2: Generate Nonces (Back-end)
For each user in the database, generate a random string in the nonce
field. For example, nonce
can be a big random integer.
Step 3: User Fetches Their Nonce (Front-end)
In our front-end JavaScript code, assuming MetaMask is present, we have access to window.web3
. We can therefore call web3.eth.coinbase
to get the current MetaMask account's public address.
When the user clicks on the login button, we fire an API call to the back end to retrieve the nonce associated with their public address. Something like a route with a filter parameter GET /api/users?publicAddress=${publicAddress}
should do. Of course, since this is an unauthenticated API call, the back end should be configured to only show public information (including nonce
) on this route.
If the previous request doesn't return any result, it means that the current public address hasn't signed up yet. We need to first create a new account via POST /users
, passing publicAddress
in the request body. On the other hand, if there's a result, then we store its nonce
.
Step 4: User Signs the Nonce (Front-end)
Once the front end receives nonce
in the response of the previous API call, it runs the following code:
web3.personal.sign(nonce, web3.eth.coinbase, callback);
This will prompt MetaMask to show a confirmation popup for signing the message. The nonce will be displayed in this popup, so that the user knows she or he isn't signing some malicious data.
When she or he accepts it, the callback function will be called with the signed message (called signature
) as an argument. The front end then makes another API call to POST /api/authentication
, passing a body with both signature
and publicAddress
.
Step 5: Signature Verification (Back-end)
When the back end receives a POST /api/authentication
request, it first fetches the user in the database corresponding to the publicAddress
given in the request body. In particular it fetches the associated nonce.
Having the nonce, the public address, and the signature, the back end can then cryptographically verify that the nonce has been correctly signed by the user. If this is the case, then the user has proven ownership of the public address, and we can consider her or him authenticated. A JWT or session identifier can then be returned to the front end.
Step 6: Change the Nonce (Back-end)
To prevent the user from logging in again with the same signature (in case it gets compromised), we make sure that the next time the same user wants to log in, she or he needs to sign a new nonce. This is achieved by generating another random nonce
for this user and persisting it to the database.
Et voilà! This is how we manage a nonce-signing passwordless login flow.
Why the Login Flow Works
Authentication, by definition, is really only the proof of ownership of an account. If you uniquely identify your account using a public address, then it's cryptographically trivial to prove you own it.
To prevent the case where a hacker gets hold of one particular message and your signature of it (but not your actual private key), we enforce the message to sign to be:
- Provided by the back end, and
- Regularly changing
We changed it after each successful login in our explanation, but a timestamp-based mechanism could also be imagined.
Let's Build It Together
In this section, I'll go through the six steps above, one by one. I'll show some snippets of code for how we can build this login flow from scratch, or integrate it in an existing back end, without too much effort.
I created a small demo app for the purpose of this article. The stack I'm using is the following:
- Node.js, Express, and SQLite (via the Sequelize ORM) to implement a RESTful API on the back end. It returns a JWT on successful authentication.
- React single-page application on the front-end.
I try to use as few libraries as I can. I hope the code is simple enough so that you can easily port it to other tech stacks.
The whole project can be seen in this GitHub repository. A demo is hosted here.
Step 1: Modify the User Model (Back-end)
Two fields are required: publicAddress
and nonce
. We initialize nonce
as a random big number. This number should be changed after each successful login. I also added an optional username
field here that the user would be able to change.
const User = sequelize.define('User', { nonce: { allowNull: false, type: Sequelize.INTEGER.UNSIGNED, defaultValue: () => Math.floor(Math.random() * 1000000) // Initialize with a random nonce }, publicAddress: { allowNull: false, type: Sequelize.STRING, unique: true, validate: { isLowercase: true } }, username: { type: Sequelize.STRING, unique: true } });
To make it simple, I set the publicAddress
field as lowercase. A more rigorous implementation would add a validation function to check that all addresses here are valid Ethereum addresses.
Step 2: Generate Nonces (Back-end)
This is done in the defaultValue()
function in the model definition above.
Step 3: User Fetches Their Nonce (Front-end)
The next step is to add some boilerplate code on the back end to handle CRUD methods on the User
model, which we won't do here.
Switching to the front-end code, when the user clicks on the login button, our handleClick
handler does the following:
class Login extends Component { handleClick = () => { // --snip-- const publicAddress = web3.eth.coinbase.toLowerCase(); // Check if user with current publicAddress is already present on back end fetch(`${process.env.REACT_APP_BACKEND_URL}/users?publicAddress=${publicAddress}`) .then(response => response.json()) // If yes, retrieve it. If no, create it. .then( users => (users.length ? users[0] : this.handleSignup(publicAddress)) ) // --snip-- }; handleSignup = publicAddress => fetch(`${process.env.REACT_APP_BACKEND_URL}/users`, { body: JSON.stringify({ publicAddress }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }).then(response => response.json()); }
Here, we are retrieving the MetaMask active account with web3.eth.coinbase
. Then we check whether this publicAddress
is already present or not on the back end. We either retrieve it, if the user already exists, or if not, we create a new account in the handleSignup
method.
Step 4: User Signs the Nonce (Front-end)
Let's move forward in our handleClick
method. We now have in our possession a user given by the back end (be it retrieved or newly created). In particular, we have their nonce
and publicAddress
. So we're ready to sign the nonce with the private key associated with this publicAddress
using web3.personal.sign
. This is done in the handleSignMessage
function.
Do note that web3.personal.sign
takes a hexadecimal representation of the string as its first argument. We need to convert our UTF-8-encoded string to hex format using web3.fromUtf8
. Also, instead of signing the nonce only, I decided to sign a more user-friendly sentence, since it will be displayed in the MetaMask confirmation popup: I am signing my once-time nonce: ${nonce}
.
class Login extends Component { handleClick = () => { // --snip-- fetch(`${process.env.REACT_APP_BACKEND_URL}/users?publicAddress=${publicAddress}`) .then(response => response.json()) // If yes, retrieve it. If no, create it. .then( users => (users.length ? users[0] : this.handleSignup(publicAddress)) ) // Popup MetaMask confirmation modal to sign message .then(this.handleSignMessage) // Send signature to back end on the /auth route .then(this.handleAuthenticate) // --snip-- }; handleSignMessage = ({ publicAddress, nonce }) => { return new Promise((resolve, reject) => web3.personal.sign( web3.fromUtf8(`I am signing my one-time nonce: ${nonce}`), publicAddress, (err, signature) => { if (err) return reject(err); return resolve({ publicAddress, signature }); } ) ); }; handleAuthenticate = ({ publicAddress, signature }) => fetch(`${process.env.REACT_APP_BACKEND_URL}/auth`, { body: JSON.stringify({ publicAddress, signature }), headers: { 'Content-Type': 'application/json' }, method: 'POST' }).then(response => response.json()); }
When the user has successfully signed the message, we move onto the handleAuthenticate
method. We simply send a request to the /auth
route on the back end, sending our publicAddress
as well as the signature
of the message the user just signed.
Step 5: Signature Verification (Back-end)
This is the slightly more complicated part. The back end receives a request on the /auth
route containing a publicAddress
and a signature
, and needs to verify if this publicAddress
has signed the correct nonce
.
The first step is to retrieve from the database the user with said publicAddress
; there is only one because we defined publicAddress
as a unique field in the database. We then set the message msg
as "I am signing my…", exactly like in the front end in Step 4, with this user's nonce.
The next block is the verification itself. There is some cryptography involved. If you feel adventurous I recommend you reading more about elliptic curve signatures.
To summarize this block, what it does is, given our msg
(containing the nonce
) and our signature
, the ecrecover
function outputs the public address used to sign the msg
. If it matches our publicAddress
from the request body, then the user who made the request successfully proved their ownership of publicAddress
. We consider them authenticated.
User.findOne({ where: { publicAddress } }) // --snip-- .then(user => { const msg = `I am signing my one-time nonce: ${user.nonce}`; // We now are in possession of msg, publicAddress and signature. We // can perform an elliptic curve signature verification with ecrecover const msgBuffer = ethUtil.toBuffer(msg); const msgHash = ethUtil.hashPersonalMessage(msgBuffer); const signatureBuffer = ethUtil.toBuffer(signature); const signatureParams = ethUtil.fromRpcSig(signatureBuffer); const publicKey = ethUtil.ecrecover( msgHash, signatureParams.v, signatureParams.r, signatureParams.s ); const addressBuffer = ethUtil.publicToAddress(publicKey); const address = ethUtil.bufferToHex(addressBuffer); // The signature verification is successful if the address found with // ecrecover matches the initial publicAddress if (address.toLowerCase() === publicAddress.toLowerCase()) { return user; } else { return res .status(401) .send({ error: 'Signature verification failed' }); } })
On successful authentication, the back end generates a JWT and sends it back to the client. This is a classic authentication scheme, and the code for integrating JWT with your back end you can find in the repo.
Step 6: Change the Nonce (Back-end)
The final step is to change the nonce, for security reasons. Somewhere after the successful authentication, add this code:
// --snip-- .then(user => { user.nonce = Math.floor(Math.random() * 1000000); return user.save(); }) // --snip--
It wasn't so hard, was it? Again, if you want to see how the whole app is wired up (JWT generation, CRUD routes, localStorage, etc.), feel free to have a look at the GitHub repo.
It's Production-ready Today
While the blockchain may have its flaws and is still in an infant stage, I can't emphasize enough how this login flow could be implemented on any existing website today. Here's a list of arguments why this login flow is preferable over both email/password and social logins:
- Increased security: Proof of ownership by public-key encryption is arguably more secure than proof of ownership by email/password or by a third party—all the more so because MetaMask stores credentials locally on your computer, and not on online servers, which makes the attack surface smaller.
- Simplified UX: This is a one-click (okay, maybe two-click) login flow, done in a handful of seconds, without the need to type or remember any password.
- Increased privacy: No email needed, and no third party involved.
Of course, a MetaMask login flow can perfectly well be used in parallel with other traditional login methods. A mapping needs to be done between each account and the public address(es) it holds.
But this login flow is not suited for everyone:
- Users need to have MetaMask installed: This login flow obviously doesn't work without MetaMask or a
web3
-enabled browser. If your audience is not interested in cryptocurrencies, there's a small chance they would even consider installing MetaMask. With the recent crypto-boom, let's hope we're heading towards a Web 3.0 internet. - Some work needs to be done on the back end: As we've seen, it's quite straightforward to implement a simple version of this login flow. However, to integrate it into an existing complex system, it requires some changes in all areas that touch authentication: Signup, database, authentication routes, etc. This is especially true because each account will be associated with one or more public addresses.
- It doesn't work on mobile: This deserves its own section—read on.
Shortcomings on Mobile
As we have seen, web3
is a prerequisite for this login flow. On desktop browsers, MetaMask injects it. However, there are no extensions on mobile browsers, so this login flow won't work out-of-the-box on mobile Safari, Chrome, or Firefox. There are some standalone mobile browsers which inject web3
—basically MetaMask wrapped up in a browser. They are pretty early-stage as of this writing, but if you are interested, have a look at Cipher, Status, and Toshi. "Login with MetaMask" works with these mobile browsers.
Concerning mobile apps, the answer is yes, the login flow works, but there's a lot of groundwork to prepare. Basically, you would need to rebuild a simple Ethereum wallet yourself. This includes public address generation, seed word recovery, and secure private key storage, as well as web3.personal.sign
and the confirmation popup. Fortunately, there are libraries to help you. The crucial area to focus on is naturally security, as the app itself holds the private key. On desktop browsers, we delegated this task to MetaMask.
So I would argue that the short answer is no, this login flow does not work on mobile today. Effort is being put in this direction, but the easy solution today remains a parallel traditional login method for mobile users.
We introduced in this article a one-click, cryptographically-secure login flow, with no third party involved, called "Login with MetaMask". We explained how a digital signature of a back end-generated random nonce can prove ownership of an account, and therefore provide authentication. We also explored the trade-offs of this login mechanism compared to traditional email/password or social logins, both on desktop and on mobile.
Even though the target audience of such a login flow is still small today, I sincerely hope that some of you feel inspired to offer Login with MetaMask in your own web app, in parallel to traditional login flows—and I would love to hear about it if you do. If you have any questions, feel free to get in touch in the comments below.
Further Reading on the Toptal Engineering Blog:
- How to Create an ERC20 Token the Simple Way
- The Ultimate ENS and ĐApp Tutorial
- Time-locked Wallets: An Introduction to Ethereum Smart Contracts
- Less Trust, More Truth: Web3 Design Best Practices and Patterns
- Integrating Facebook Login in AngularJS App with Satellizer
Source: https://www.toptal.com/ethereum/one-click-login-flows-a-metamask-tutorial
0 Response to "On My Blockchain Why is the Continue Button Not Available"
إرسال تعليق