Signing In With Ethereum: Owning Your Own Identity

Aw Kai Shin
8 min readSep 29, 2022

--

SIWE

With public key cryptography underlying Web3, one of the novel use cases which has risen is the ability to verify “who you say you are” without the need of a centralised identity provider. While there are many pieces being concurrently built in the rapidly evolving decentralised identity space, SpruceID is leading the charge on the authentication front.

SIWE (Sign In With Ethereum) has quickly established itself as an essential tool for users who demand more autonomy over their own digital identity. This is achieved through providing users the option to self-custodise their own digital identity via an EVM compatible wallet (ie. signing a verifiable message). Moreover, by establishing a standard sign-in workflow, SIWE can be easily integrated across existing identity services and is therefore not limited to just Web3 native applications.

SpruceID has provided a very useful SIWE Notepad Example which implements an end-to-end SIWE authentication on Express.js, complete with session management.

siwe-notepad

The purpose of this guide is to focus on just the core sign-in flow using Metamask. By setting aside session management as well as integration with other wallet providers, I hope to simplify the core aspects of SIWE :

  • SIWE Authentication on Client vs Server
  • Constructing the sign-in message for users to approve
  • Avoiding replay attacks through nonce synchonisation
  • Interacting with SIWE via the Metamask UI

The Github repository for this guide can be found here. If you would like an introduction to programatically interacting with Metamask:

Setting up an application skeleton

We first need to create a project directory and install Express:

mkdir MetamaskSIWE
cd MetamaskSIWE
npm install express

With the Express package installed, we can then utilise express-generator to quickly and conveniently setup an app template. Before running express-generator, we can view the setup options by running the following:

npx express-generator --help

For our purposes, we want to bootstrap our project using EJS as this minimises the need to context switch given its similarities to HTML. As such, we will run express-generator with the EJS view flag:

npx express-generator -v ejs

As instructed, we will also install the required dependencies:

npm install

For the sake of convenience, we will be using nodemon to automatically refresh our application whenever we make any changes. The command below will install nodemon globally so that you can use it for your other projects:

npm install -g nodemon

Once installed, we can start our web application by running the below:

nodemon start

You should then be able to see a welcome page below when visiting the default port at localhost:3000.

Express is now setup and we can go ahead and make the necessary changes to connect Metamask to our Express instance.

Create Login with SIWE Button

The first thing we will do is to change the view so that the page display is more intuitive for our purpose. Navigate into the index.ejs file located in the /views folder. We can replace the <body> with the code below:

As per Metamask best practice, we have included a button for the user to initiate the connection request. The connection request should always be initiated by the user and not on page load.

Additionally, we have also included a bundle.js script which we will be creating shortly. This script will hold the code required for Metamask to connect to the application.

Lastly, we have also changed the title that is fed into the page by modifying index.js under the /routes folder. The res.render() feeds the data passed in the function to our index.ejs file to be rendered.

Saving the above, nodemon would have automatically refreshed your application and your browser should display the following:

We can now get started on implementing the Ethereum login logic.

Using SIWE to Sign-in from the Browser

In order to utilise the SIWE library, we will first install it via:

npm install siwe

Do note that SIWE comes pre-packaged with Ethers.js and hence there is no need to install Ethers.js again else there would be a conflict.

As we will need to run this script from the browser, we will also be using Browserify in order to require the modules on the client’s browser. Refer to the Browserify section of the previous guide for more details on this.

Consequently, we will first create an input file, provider.js, which contains all the logic required for this guide. This file will then be “Browserified” with the output file, bundle.js, being placed in the /public/javascripts/ directory.

touch provider.js

We can then copy and paste the following into our provider.js file:

We first instantiate our button by using the query selector and add a click event listener on the button. Once clicked, a few things will happen:

  • Extract the provider and signer from the browser window using Ethers.js. This allows us to programatically interact with our Metamask instance.
  • Get the active Metamask account. This will be the address which is linked to the sign-in request.
  • Generate a randomized nonce which will be used to validate the sign-in between the server and the client. Note that this is NOT the account nor consensus nonce which is used in Ethereum. To avoid the nonce being manipulated, this is generated on the server via calling the /api/nonce endpoint.
  • Create a new SIWE message which will allow us to call the SIWE library methods on the message. You can find the code here.
  • Prompt the user to sign the prepared SIWE message which will generate a receipt containing the raw signature of the message. You can refer to the Ethers.js signMessage() function here.
  • Call the /api/sign_in endpoint in order for the server to validate and handle the sign-in. As per session management best practices, we will be using the POST method here so that sensitive information is embedded in the request body.

Save the providers.js file and we can then run the Browserify command:

browserify providers.js -o public/javascripts/bundle.js

A bundle.js file will be created in the public directory which static assets are served from. Note that our index.ejs file also contains a script tag referencing this path. Although nodemon would have automatically refreshed our app, we still need to implement the /api/nonce and /api/sign_in before our sign-in is fully functional.

Updating our Server’s Endpoints

We will now switch to our index.js file located under the routes/ directory to add our 2 endpoints.

The /api/nonce endpoint will utilise the generateNonce() function from the SIWE library which generates a sufficiently random nonce to prevent replay attacks. The code for generateNonce() can be found here. Our endpoint will respond with the generated nonce:

Note that we have also initialised nonce at the file level instead of saving it to a session for the sake of legibility. The nonce is stored on the server in order to be able to compare it with the nonce returned from the client when a user logs in.

The majority of the sign in code will be handled by the /api/sign_in endpoint:

As per our client code, this endpoint is implemented as a POST request in order to minimise data leakages (this will have to be paired with HTTPS for better security). Consequently, we first need to destructure the request in order to get the message and signature. The message is then formatted into the SiweMessage equivalent. This SiweMessage should contain the same information as the one generated in the browser.

In order to validate the signature and message pair, we also require a new provider instance to be created. In this case, we will use Ethers.js default provider as we just need a quick way to test the sign in function.

The user login is then validated using the validate() method on the SiweMessage equivalent. The latest implementation can be found on SIWE’s Github. Do note that validate() is soon to be deprecated in favour of verify() which implements additional safeguards. However, as this latest change have yet to be implemented in NPM, this guide will still use the validate() function.

Upon validation, the results are written to fields. Critically, our code also implements a check to ensure that fields.nonce is equivalent to the nonce which was generated prior upon /api/nonce being called. Once this check is done, the user has successfully signed in and we can handle the session accordingly. Additionally, a success response is also returned to the client which will be logged in the browser console.

Signing In With Metamask UI

We can now navigate our browser to test the sign in via the user interface. Click the “Login with SIWE” button and you should receive a signature request from Metamask.

Notice that the message defined in our provider.js is displayed for the user to review. Additionally, you will also see the console printing relevant details such as the nonce and the SiweMessage.

You should also see the equivalent nonce being generated and printed to your server logs.

We can then click the “Sign” button to approve this transaction. This will create the signed message which is printed to the browser’s console.

With this signature, the /api/sign_in endpoint will also be triggered resulting in the following being printed to our server console:

Once completed, the success message returned by the server will be reflected accordingly in the browser console:

Congrats, you have successfully implemented signing in with Ethereum using Metamask!

Thanks for staying till the end. Would love to hear your thought/comments so do drop a comment. I’m active on twitter @AwKaiShin if you would like to receive more digestible tidbits of crypto-related info or visit my personal website if you would like my services :)

--

--

Aw Kai Shin

Web3, Crypto & Blockchain: Building a More Equitable Web | Technical Writer @FactorDAO | www.awkaishin.com