November 29th 2021

Web3 Authentication

Financial reasons aside, here is why im obsessed with crypto and passwordless login:

Personally I use bitwarden to manage the mess that is my username-password combo-hell. And although I would highly recommend bitwarden to anyone who still even remotely attempts to remember a single of their “iMissMyMom123” passwords. After having logged in to a server over SSH using ssh keys, my world changed.

Metamask

With the rise of web3 technologies and the ethereum blockchain a wide variety of tools have been released that allow us to interface with our beloved chains in many different ways. But I would honestly say that the most prevalent of them all, has to be metamask. Metamask is accessible through the window.ethereum provider and technically speaking you could directly interface with this api in order to achieve the same effect as I will be describing here.

Web3-react

Now this is where web3-react comes in. web3-react describes itself as “a simple, maximally extensible, dependency minimized framework for building modern Ethereum dApps”, which, seems self-explanatory. In essence, web3-react provides you with a plethera of ways to interface with your favorite chains aswell as “connecting” with metamask.

Usage

Let’s get to some practical applications shall we? For the following, I will assume you have a react project already setup, wether you did this using CRA or wether you made it yourself is fully up to you. In my example I will be using parcel as a bundler, following my own react-template.

Dependencies

For basic project setup we will need some dependencies, but not to worry, simply run the following command.

yarn add @ethersproject/providers @web3-react/core @web3-react/injected-connector web3-react

Context

Now that we have our dependencies installed, at the most most highest level of your code you will want to wrap everything in a Web3ReactProvider like so:

import { Web3Provider } from "@ethersproject/providers";
import { Web3ReactProvider } from "@web3-react/core";
import { render } from "react-dom";
import { App } from "./App";

const getLibrary = async (provider) => {
  const library = new Web3Provider(provider);
  return library;
};

render(
  <Web3ReactProvider getLibrary={getLibrary}>
    <App />
  </Web3ReactProvider>,
  document.getElementById("root")
);

In the above code we essentially tell web3-react to use the ethers library in the backend, which then does most of the interfacing for us.

useAuth hook

In most cases I prefer to create my own hook, that depends on the web3-react hooks in order for a more controlled flow and more extensibility later on. Let’s create a file called useAuth.ts with the following contents.

import { Web3Provider } from "@ethersproject/providers";
import { useWeb3React } from "@web3-react/core";

export const useAuth = () => {
  const web3 = useWeb3React<Web3Provider>();

  return {
    ...web3,
  };
};

Yes, I understand that the above code seems to be very redundant, but I promise, you will most likely want to add something in here, such as a useEffect hook, or similar, to check if the user is already logged in etc. Or maybe even add extra data, such as ENS or other things of your interest.

Authentication

Now that we have our useAuth hook we will be able to start using it in places.

import { useAuth } from "../util/useAuth";
import { InjectedConnector } from "@web3-react/injected-connector";

export const AuthTest = () => {
  const { active, activate, account, chainId } = useAuth();

  if (!active)
    return (
      <div
        onClick={() => {
          activate(new InjectedConnector({}));
        }}
      >
        Connect Wallet
      </div>
    );

  return (
    <div>
      <p>You are logged in!</p>
      <p>Account: {account}</p>
      <p>Chain: {chainId}</p>
    </div>
  );
};

In the above code sample, we have a component that renders the text “Connect Wallet” in the event that the user is not connected yet. Once the user interacts with this text the metamask connection is triggered and metamask will run you through the procedure of connecting. Once you are connected the component will re-render and now display your public key aswell as the current chain you are connected to.

From here on out all future updates in metamask will trigger a rerender in any component that makes use of the useAuth hook. As you can see now, we have basic authentication working with public key.

Security Notice

However the above setup works perfectly as described, it is important to note that we cannot rely 100% on the frontend to be trustworthy. Technically speaking it is possible for an attacker to just force a different address and “log in” to said account. All data that is displayed on the app in the current phase ought to be treated as “public”-information and in the web3 world ought to obviously come from some form of blockchain.

If you do however wish to build a centralized application, with a backend etcetera then most likely you will want to have a look at the next section of this document.

Signing

Although the above workflow works very nice and smoothly we still cannot 100% verify that this user is legitimate, that is where the next step comes in. Luckily, web3-react and ethers have made it fairly easy for us because of their exposed signing api.

The flow we would like to aim for here will be as follows. The user sends a request to the backend stating that “they would like to log in”, and most likely include their public-key in the message. The backend shall then return a nonce, or challenge, if you will, any random value will do. This message arrives at the frontend and we can then use the following.

import { useAuth } from "../util/useAuth";

export const AuthTest = () => {
  const { library } = useAuth();

  return (
    <div
      onClick={async () => {
        const signature = await library
          .getSigner()
          .signMessage("Your message from backend here.");
        console.log(signature);
      }}
    >
      Click me to sign a message
    </div>
  );
};

The user will now be prompted with an additional dialogue asking for it to sign the message you have provided, most likely this will look something like so.

Success! The code above should return the signature and you can now send this to your backend! Your backend can use the signature and your public key to verify that you did actually sign the message and return you a JWT token or anything of the sorts. Hooray! You now have passwordless login!

Final

Although we are obviously nowhere near a “smooth” transition from using passwords everywhere to using public-key authentication. I am really looking forward a foreseable future inwhich we can all benefit from this. A time when practices like phishing or bad password quality are not an issue anymore. Personally I think that the driving force behind pushing this method of authentication to the next level is browser-adoption. In its current form brave has an integrated version of metamask, but most modern browsers will still require your users to actually install the metamask browser extension. Hopefully, as we have seen with google moving to 2FA Hardware Keys causing a sudden browser implementation of the CredentialsContainer. Util then, I’ll be counting down the days.

~Luc