Moving From SIWE to SSX

A tutorial on integrating SSX

Overview

Sign-in with Ethereum is a great way to enable your users to authenticate using their Ethereum wallet. However, adding it you your application requires adding logic and endpoints for managing sessions, connecting to the Ethereum network, issuing nonces, and verifying signatures. Adding or extending functionality around sign-in (such as signing in on behalf of a Gnosis Safe) can also be tricky to figure out.

This is where SSX comes in! SSX is a library that makes it easy to add SIWE to your application. It handles all the logic and endpoints for you and allows you to easily extend it to add additional functionality such as signing in on behalf of a Gnosis Safe, resolving ENS names, and more.

This guide demonstrates how to add SSX to an application that is already using the SIWE library.

Modifications

ssx-notepad is a simple example dapp with a simple express backend that stores a note for an authenticated Ethereum user. It is based on the original siwe-notepad.

The completed version of the dapp with the modifications can be found here:

To modify ssx-notepad to use SSX instead of SIWE the following files were changed:

  • src/providers.ts - this file holds the frontend code including the wallet connection logic.

  • src/index.ts - this file handles the backend logic including the endpoints for signing in and storing the note.

Migration

Change Overview

The migration from SIWE to SSX is fairly straightforward. The changes can be seen in ssx-notepad which was forked from the siwe-notepad example. The main changes are:

  • Add the ssx library to your frontend (4575af9).

  • Replace the wallet connection and signing logic with the ssx library (391a90f).

  • Add the ssx-server library to your backend (d484d0e).

  • Create and instantiate an SSX object and middleware (a7ff4a5).

  • Point the frontend SSX client to your SSX backend (301ca42).

  • Use SSX for session authentication (390a2ec).

  • Update/verify your API calls work with the session cookie.

  • Remove the SIWE library and unused functions/endpoints.

Frontend Changes

You can follow along by forking or cloning siwe-notepad and applying them yourself. If cloning siwe-notepad, be sure to add an .env file with an SSX Signing Key.

First, you'll need to install SSX and add it you your project:

cd siwe-notepad
npm install @spruceid/ssx

In src/providers.tsadd the following:

import { SSX } from "@spruceid/ssx";

Replace the wallet connection and signing logic with the ssx library (making sure to note the location by using the provided comments):

let ssx: SSX | undefined; // global/reusable scope

/** ...in the signIn function
/* walletconnect.enable();
/*   provider = new ethers.providers.Web3Provider(walletconnect);
/* }
/**/

ssx = new SSX({
  providers: {
    web3: { driver: provider },
  },
  siweConfig: {
    domain: "localhost:4361",
    statement: "SIWE Notepad Example",
  },
});

let { address, ens } = await ssx.signIn();

/** ...previous code
/* const [address] = await provider.listAccounts();
/**/

If you are following by editing siwe-notepad, be sure to remove duplicate variables address and ens, as seen in this change.

Check that your application still works as expected. You will have two SIWE calls, the new one from SSX and the existing one.

We will come back to the frontend to connect it and clean up after we add ssx-server.

Backend Changes

Now, we'll add install and SSX to the backend server. First, install ssx-server via your terminal.

npm install @spruceid/ssx-server

In src/index.tsadd the following (making sure to note the location by using the provided comments):

import {
  SSXServer,
  SSXExpressMiddleware,
  SSXInfuraProviderNetworks, // optional enum import
  SSXRPCProviders, // optional enum import
} from "@spruceid/ssx-server";

/** SSX instantiated after env import
/* config();
/**/

const ssx = new SSXServer({
  signingKey: process.env.SSX_SIGNING_KEY, // you will need to add this to your .env
  providers: {
    // an RPC provider is optional here, as we are not resolving ens server side. But this is supported
    rpc: {
      service: SSXRPCProviders.SSXInfuraProvider,
      network: SSXInfuraProviderNetworks.MAINNET,
      apiKey: process.env.INFURA_API_KEY ?? "",
    },
    sessionConfig: {
      store: () => {
        // we use the existing configuration for session store, but pass it to SSX Server
        return new FileStoreStore({
          path: Path.resolve(__dirname, "../db/sessions"),
        });
      },
    },
  },
});

// Express Middleware: place after app = Express();
app.use(SSXExpressMiddleware(ssx));

If following the siwe-notepad example, you can update the warning about required secrets.

Once SSX is live on the server, you can add it to your frontend by adding the providers.server.host field.

Once this is done, you should be able to sign in and get a session cookie from the server using SIWE:

ssx = new SSX({
 providers: {
   web3: { driver: provider },
+  server: { host: "/" },   
 },
 siweConfig: {
   domain: "localhost:4361",
   statement: "SIWE Notepad Example",
 },
});

Now that SSX is installed, you can use it to protect express routes instead of req.session.siwe:

app.put('/api/save', async (req, res) => {
-   if (!req.session.siwe) {
+   if (!req.ssx.verified) {
       res.status(401).json({ message: 'You have to first sign_in' });
       return;
   }

Cleanup and Testing

Once you've done the above steps, SSX is now installed for your dapp! After this you can do the following:

  • Remove logic from the frontend that uses the old authentication. Verify that the session cookie is sent with your requests to the server. Test the dapp!

  • Remove or deprecate old endpoints and logic using the previous SIWE method on your backend.

  • Remove the siwe dependency.

Frontend Cleanup

If following along with siwe-notepad, remove this old code to get user data:

    const [address] = await provider.listAccounts();
    if (!address) {
        throw new Error('Address not found.');
    }

    /**
     * Try to resolve address ENS and updates the title accordingly.
     */
    let ens: string;
    try {
        ens = await provider.lookupAddress(address);
    } catch (error) {
        console.error(error);
    }

And replace this code to login with only ssx

    let { address, ens } = await ssx.signIn();
+   updateTitle(ens?.domain || address);
+   const res = await fetch(`/api/me`);
-    /**	
-     * Gets a nonce from our backend, this will add this nonce to the session so	
-     * we can check it on sign in.	
-     */	
-    const nonce = await fetch('/api/nonce', { credentials: 'include' }).then((res) => res.text());	
-
-    /**	
-     * Creates the message object	
-     */	
-    const message = new SiweMessage({	
-        domain: document.location.host,	
-        address,	
-        chainId: await provider.getNetwork().then(({ chainId }) => chainId),	
-        uri: document.location.origin,	
-        version: '1',	
-        statement: 'SIWE Notepad Example',	
-        nonce,	
-    });	

-    /**	
-     * Generates the message to be signed and uses the provider to ask for a signature	
-     */	
-    const signature = await provider.getSigner().signMessage(message.prepareMessage());
-    fetch(`/api/sign_in`, {
-        method: 'POST',
-        body: JSON.stringify({ message, ens, signature }),
-        headers: { 'Content-Type': 'application/json' },
-        credentials: 'include',
-    }).then(async (res) => {
        if (res.status === 200) {
            res.json().then(({ text, address, ens }) => {
                connectedState(text, address, ens);

Other SSX Features

SSX has a few great features like signing in on behalf of a Safe, resolving ENS on login, or accessing metrics. To learn more about enabling these features, check out the SSX Docs.

ENS Resolution

Adding a flag for ENS resolution will also provide an ENS domain and name if one is set for the account:

ssx = new SSX({
+ resolveEns: true,
  providers: {
    web3: { driver: provider },
  },
  siweConfig: {
    domain: "localhost:4361",
    statement: "SIWE Notepad Example",
  },
});
let { address, ens } = await ssx.signIn(); // this will now show if ens exists

Last updated