Implement the Backend

Here we learn how to build the backend server to handle the user's submission using Express.js.

A completed version of this part can be found here (02_backend). This example uses only uses the command line in the terminal to print messages, no monitoring of the browser console log is necessary.

The backend server gives the frontend a nonce to include in the SIWE message and also verifies the submission. As such, this basic example only provides two corresponding endpoints:

  • /nonce to generate the nonce for the interaction via GET request.

  • /verify to verify the submitted SIWE message and signature via POST request.

While this simple example does not check the nonce during verification, all production implementations should, as demonstrated in the final section.

1. Setup the project directory:

mkdir siwe-backend && cd siwe-backend/
yarn init --yes
mkdir src/
yarn add cors express siwe ethers

2. Make sure that the package.json type is module like the following:

{
  "name": "backend",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "license": "MIT",
  "scripts": {
    "start": "node src/index.js"
  },
  "dependencies": {
    "siwe": "^2.1.4",
    "cors": "^2.8.5",
    "ethers": "^6.3.0",
    "express": "^4.18.2"
  }
}

3. Populate src/index.js with the following:

src/index.js
import cors from 'cors';
import express from 'express';
import { generateNonce, SiweMessage } from 'siwe';

const app = express();
app.use(express.json());
app.use(cors());

app.get('/nonce', function (_, res) {
    res.setHeader('Content-Type', 'text/plain');
    res.send(generateNonce());
});

app.post('/verify', async function (req, res) {
    const { message, signature } = req.body;
    const siweMessage = new SiweMessage(message);
    try {
        await siweMessage.verify({ signature });
        res.send(true);
    } catch {
        res.send(false);
    }
});

app.listen(3000);

4. You can run the server with the following command.

yarn start

In a new terminal window, test the /nonce endpoint to make sure the backend is working:

curl 'http://localhost:3000/nonce'

In the same new terminal window, test the /verify endpoint use the following, and ensure the response is true:

curl 'http://localhost:3000/verify' \
  -H 'Content-Type: application/json' \
  --data-raw '{"message":"localhost:8080 wants you to sign in with your Ethereum account:\n0x9D85ca56217D2bb651b00f15e694EB7E713637D4\n\nSign in with Ethereum to the app.\n\nURI: http://localhost:8080\nVersion: 1\nChain ID: 1\nNonce: spAsCWHwxsQzLcMzi\nIssued At: 2022-01-29T03:22:26.716Z","signature":"0xe117ad63b517e7b6823e472bf42691c28a4663801c6ad37f7249a1fe56aa54b35bfce93b1e9fa82da7d55bbf0d75ca497843b0702b9dfb7ca9d9c6edb25574c51c"}'

Note on Verifying Messages

We can verify the received SIWE message by parsing it back into a SiweMessage object (the constructor handles this), assigning the received signature to it and calling the verify method:

message.verify({ signature })

message.verify({ signature }) in the above snippet makes sure that the given signature is correct for the message, ensuring that the Ethereum address within the message produced the matching signature.

In other applications, you may wish to do further verification on other fields in the message, for example asserting that the authority matches the expected domain, or checking that the named address has the authority to access the named URI.

A small example of this is shown later where the nonce attribute is used to track that a given address has signed the message given by the server.

Last updated