Setting Up SpruceID SSX + Integration Into An Existing Dapp!

Setting Up SpruceID SSX + Integration Into An Existing Dapp!

What is SSX?

A really simple way to put it is this: it is Sign in with Ethereum but with user session data. It comes with most, if not all of the functionalities of SIWE, and then some (session data monitoring). Dapps admins can monitor a user’s behavior on their dapp by going over the log data at http://app.ssx.id.

As a user, the introduction of SSX (SIWE), ensures that the user’s data stays private. I am quite particular about privacy, and so is the Spruce team, the team behind SIWE & SSX. As we move towards a decentralized social landscape, it is important that users can choose to log in with their Ethereum wallet, and carefully select what data to share with web apps. As opposed to social logins that track your every movement. That was a mini rant about privacy, and one of the problems SSX solve. The tool is also open-sourced, and the dashboard is fire! Let’s write some code!

Setting Up SSX

If you want to simply learn how to set up a stand-alone dapp with SSX, codingwithmanny already wrote a detailed article on that over at the Developer Dao’s Hasnode blog. You can learn more about it here.

In this article, we’re integrating SSX into an existing “web2” style login web app system. We have the typical email and password log-in and sign-out.

The goal is to provide the same level of privileges to a user regardless of what method they choose to log in with. SIWE, or the traditional (archaic) email, password, type login. Let’s get to it!

For this demo, we’re working with a NodeJS, expressJs app, and JWT for user authentication. I really wanted to keep this as simple as possible, so there is no database to store user data. Most of it is hardcoded, but we’re able to store the user session cookie in the browser effectively, so it’s a W.

Here’s my web2 app. Everything works as expected. Certain routes are only accessible to logged-in users, and others you see when logged in. We’re going to be integrating SSX and granting it the same privileges.

What we currently have:

What we’ll have by the time we’re done:

The ability to login in with either account. Super cool.

Keeping this simple and straight to the point. Here are the GitHub links:

  1. The Standalone ‘web2’ app without SSX: Here

  2. The full App with SSX integrated: Here

For the second file, you can quite literally clone the repo, install the dependencies, get your API keys from Infura and SSX (read about that here: ), and run the application without any hassle.

We’ll be working with No1 for this tutorial, as it is a boilerplate for what we need to get done. The first option only has a fully functional web2 login page. By the time we’re done with this article, you would have successfully integrated SSX into your app, and you can begin introducing web3 features to your users!

Important: Please go through this article to generate your API keys from SSX and Infura. You’ll be needing them

Clone the repo

git clone https://github.com/geniusyinka/Full-functional-expressReact-app.git
cd 'express-react'
open with vscode

Inside the folder, you’ll see two additional folders; client & server. The ‘client’ is your react frontend application. While the ‘server’ contains your backend logic. The next step will be to cd into these folders individually and run:

npm install

This will install all of your dependencies.

The next step will be to have SSX set up on both the client and server side. We’ll start with the ‘client’. cd into your client directory and run:

npm install @spruceid/ssx
# or
yarn add @spruceid/ssx

Then import SSX into your App.tsx file like so:

import { SSX } from '@spruceid/ssx';
import getSSXConfig from './ssx.config';

Then paste this before your App() component:

...
function AccountInfo({ address, delegator }: { address: string, delegator?: string }) {
  return (
    <div className="App-account-info">
      <h2>
        Account Info
      </h2>
      {
        address &&
        <p>
          <b>
            Address
          </b>
          <br />
          <code>
            {address}
          </code>
        </p>
      }
    </div>
  );
};

The next thing will be to instantiate SSX. Paste this code inside your App() component:

// ssx stuff!

  const [ssxProvider, setSSX] = useState<SSX | null>(null);

  const ssxHandler = async () => {
    const ssxConfig = await getSSXConfig();
    const ssx = new SSX(ssxConfig);
    await ssx.signIn();
    setSSX(ssx);
    (window as any).ssx = ssx;
  };

  const ssxLogoutHandler = async () => {
    ssxProvider?.signOut();
    setSSX(null);
  };

Then Paste this into your return method:

        <div className="App-title">
          <h1>SSX Example Dapp</h1>
          <p>Sign in with your email and password below</p>
          <div className="App-content">
            {
              ssxProvider ?
                <>
                  <button onClick={ssxLogoutHandler}>
                    SIGN OUT
                  </button>
                  <AccountInfo
                    address={ssxProvider?.address() || ''}
                  />
                </> :
                <button onClick={ssxHandler}>
                  SIGN-IN WITH ETHEREUM
                </button>
            }
          </div>
            </div>

Create a .env file and paste this into the parent client directory:

REACT_APP_SSX_DAO_LOGIN=true
REACT_APP_SSX_METRICS_SERVER=http://localhost:5001

One tiny feature that helps with cors and api calls is to add a proxy URL to our Package.json, if it isn’t already there. Open Package.json and paste this preferably after ‘version’

{
...
"proxy": "<http://localhost:5001>",
...
}

The final step on the client side will be to create a ssx.config.js file if it doesn’t exist already. Paste this in there:

const getSSXConfig = async () => {

  return { 
        enableDaoLogin: !!(process.env.REACT_APP_SSX_DAO_LOGIN === "true"),
        // ADD THIS LINE
        providers: { server: { host: process.env.REACT_APP_SSX_METRICS_SERVER ?? "" } },
  };
};

export default getSSXConfig;

And that’s all we need to do! Your entire App.tsx should look like this:

// @ts-nocheck

import React, { useState, useEffect } from 'react'
import { SSX } from '@spruceid/ssx';
import getSSXConfig from './ssx.config';
import './App.css';

function AccountInfo({ address, delegator }: { address: string, delegator?: string }) {
  return (
    <div className="App-account-info">
      <h2>
        Account Info
      </h2>
      {
        address &&
        <p>
          <b>
            Address
          </b>
          <br />
          <code>
            {address}
          </code>
        </p>
      }
    </div>
  );
};

function App() {
  const [backendData, setBackendData] = useState([{}])
  const [logged, setLogged] = useState('')

  //login

  const onSubmitHandle = (e) => {
    const data = new FormData(e.target)
    e.preventDefault();
    const user = {}

    for (let entry of data.entries()) {
      user[entry[0]] = entry[1]
    }
    fetch('/in', {
      method: 'POST',
      body: data
    })
      .then(res => res.text())
      .then(txt => {
        if (txt == "OK") {
          setLogged('true');
          console.log('logged in!')
        }

        else { alert(txt); }
        console.log(logged)
      })
      .catch(err => console.error(err));
    return false;
  }

  useEffect(() => {
    fetch('/api')
      .then((res) => res.json())
      .then((data) => {
        setBackendData(data)
      })
  }, [])

  const console1 = () => {
    console.log(backendData)
  }

  // ssx stuff!

  const [ssxProvider, setSSX] = useState<SSX | null>(null);

  const ssxHandler = async () => {
    const ssxConfig = await getSSXConfig();
    const ssx = new SSX(ssxConfig);
    await ssx.signIn();
    setSSX(ssx);
    (window as any).ssx = ssx;
  };

  const ssxLogoutHandler = async () => {
    ssxProvider?.signOut();
    setSSX(null);
  };

  return (
    <div className="App">
      <div className="App-header">
        <div className="App-title">
          <h1>SSX Example Dapp</h1>
          <p>Connect and sign in with your Ethereum wallet or email and password below</p>
          <div className="App-content">
            {
              ssxProvider ?
                <>
                  <button onClick={ssxLogoutHandler}>
                    SIGN OUT
                  </button>
                  <AccountInfo
                    address={ssxProvider?.address() || ''}
                  />
                </> :
                <button onClick={ssxHandler}>
                  SIGN-IN WITH ETHEREUM
                </button>
            }
          </div>
        </div>

        <div className="web2">
          {!logged ?
            <>
              <form onSubmit={onSubmitHandle}>
                <input type="email" name="email" value="jon@doe.com" /> <br />
                <input type="password" name='password' value='111111' />
                <button type='submit'>submit</button>
              </form>
            </>
            :
            <>
              <form method="post" action="/out">
                <p>Welcome!</p>
                <button type='submit'>sign out</button>
              </form>
            </>
          }
        </div>
      </div>
    </div>
  );
}

export default App;

Now to the server:

cd into the server directory and run:

npm install @spruceid/ssx-server
# or
yarn add @spruceid/ssx-server

Import these:

import { utils } from 'ethers';
import { SSXServer, SSXExpressMiddleware, SSXRPCProviders, SSXInfuraProviderNetworks } from '@spruceid/ssx-server';
...

Paste this underneath the // Paste SSX Instantiation below comment:

...
dotenv.config();

const ssx = new SSXServer({
  signingKey: process.env.SSX_SIGNING_KEY,
  providers: {
    rpc: {
      service: SSXRPCProviders.SSXInfuraProvider,
      network: SSXInfuraProviderNetworks.GOERLI,
      apiKey: process.env.INFURA_API_KEY ?? "",
    },
    metrics: {
      service: 'ssx',
      apiKey: process.env.SSX_API_TOKEN ?? ""
    },
  }
});

...

Search for // SSX Begins, and paste this below:

...
app.use(SSXExpressMiddleware(ssx));

app.get('/', (req: Request, res: Response) => {
  res.send('Express + TypeScript Server');
});

app.get('/userdata', async (req: Request, res: Response) => {
  //the below code will return a false message if the user isn't logged in via either method.
  if ((!req.ssx.verified && !jwtVerify(req.cookies)))  {
    return res.status(401).json({ success: false, message: 'Unauthorized' });
  }

  //if success message = true for either web3 or web2, load this:
  const data = await getDataFromNode(req.ssx.siwe?.address);

  res.json({
    success: true,
    userId: req.ssx.siwe?.address,
    access: 'Authorized!',
    message: 'Some user data, retrieved from a blockchain node the server can access.',
    ...data,
  });
});

app.use((req, res) => {
  if (!res.headersSent) {
    res.status(404).json({ message: 'Invalid API route', success: false });
  }
});

app.listen(port, () => {
  console.log(`⚡️[server]: Server is running at <http://localhost>:${port}`);
});

async function getDataFromNode(address: string | undefined) {
  if (!address) {
    return {};
  }
  const balanceRaw = await ssx.provider.getBalance(address);
  const balance = utils.formatEther(balanceRaw);
  const currentBlock = await ssx.provider.getBlockNumber();
  return { balance, currentBlock };
}

Create a .env file and paste this into it:

PORT=5001
INFURA_API_KEY=YOUR_INFURA_KEY
SSX_API_TOKEN=SSX_API_TOKEN
SSX_SIGNING_KEY=WORKING

Your entire server.ts should look like this:

import express, { Express, Request, Response } from 'express';
import dotenv from 'dotenv';
import cors from 'cors';
import { utils } from 'ethers';
import { SSXServer, SSXExpressMiddleware, SSXRPCProviders, SSXInfuraProviderNetworks } from '@spruceid/ssx-server';
const path = require('node:path');

const bcrypt = require("bcryptjs"),
      bodyParser = require("body-parser"),
      cookieParser = require("cookie-parser"),
      multer = require("multer"),
      jwt = require("jsonwebtoken");

const app: Express = express();
const port = process.env.PORT || 5000;

//paste ssx instantiation below

dotenv.config();

const ssx = new SSXServer({
  signingKey: process.env.SSX_SIGNING_KEY,
  providers: {
    rpc: {
      service: SSXRPCProviders.SSXInfuraProvider,
      network: SSXInfuraProviderNetworks.GOERLI,
      apiKey: process.env.INFURA_API_KEY ?? "",
    },
    metrics: {
      service: 'ssx',
      apiKey: process.env.SSX_API_TOKEN ?? ""
    },
  }
});

//login


// (A2) EXPRESS + MIDDLEWARE
app.use(multer().array());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());

// (B) USER ACCOUNTS
// bcrypt.hash("111111", 8, (err, hash) => { console.log(hash); });
const users = {
  "jon@doe.com" : "$2a$08$HwENa0B1VJxPGYhzDhNsQeFjYQCf8Pwu6hGG6HJ/yyAhLKOA28AFG"
};

// (C) JSON WEB TOKEN
// (C1) SETTINGS - CHANGE TO YOUR OWN!
const jwtKey = "007",
      jwtIss = "geniusyinka",
      jwtAud = "<http://localhost:3000>",
      jwtAlgo = "HS512";

// (C2) GENERATE JWT TOKEN
var  jwtSign = email => {
  // (C2-1) RANDOM TOKEN ID
  let char = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&_-", rnd = "";
  for (let i=0; i<16; i++) {
    rnd += char.charAt(Math.floor(Math.random() * char.length));
  }

  // (C2-2) UNIX NOW
  let now = Math.floor(Date.now() / 1000);

  // (C2-3) SIGN TOKEN
  return jwt.sign({
    iat : now, // issued at - time when token is generated
    nbf : now, // not before - when this token is considered valid
    exp : now + 3600, // expiry - 1 hr (3600 secs) from now in this example
    jti : rnd, // random token id
    iss : jwtIss, // issuer
    aud : jwtAud, // audience
    data : { email : email } // whatever else you want to put
  }, jwtKey, { algorithm: jwtAlgo });
};
// console.log(jwt.sign.nbf)


// (C3) VERIFY TOKEN
 var jwtVerify = (cookies: any) => {
  if (cookies.JWT===undefined) { return false; }
  try {
    let decoded = jwt.verify(cookies.JWT, jwtKey);
    // DO WHATEVER EXTRA CHECKS YOU WANT WITH DECODED TOKEN
    // console.log(decoded);
    return true;
  } catch (err) { return false; }
}

// (D) EXPRESS HTTP
// (D1) STATIC ASSETS
app.use("/assets", express.static(path.join(__dirname, "assets")))

// (D2) HOME PAGE - OPEN TO ALL
app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "/1-home.html"));
});


// (D3) ADMIN PAGE - REGISTERED USERS ONLY
app.get("/admin", (req, res) => {
  if (jwtVerify(req.cookies)) {
    res.sendFile(path.join(__dirname, "/2-admin.html"));
  } else {
    res.json({message: 'you need to login'})
  }
});

// (D4) LOGIN PAGE
app.get("/login", (req, res) => {
  if (jwtVerify(req.cookies)) {
    res.redirect("../admin");
  } else {
    res.json({message: 'login page'})
  }
});

// (D5) LOGIN ENDPOINT
app.post("/in", async (req, res) => {
  let pass = users[req.body.email] !== undefined;
  if (pass) {
    pass = await bcrypt.compare(req.body.password, users[req.body.email]);
  }
  if (pass) {
    res.cookie("JWT", jwtSign(req.body.email));
    res.status(200);
    res.send("OK");
  } else {
    res.status(201);
    res.send("Invalid user/password");
  }
});

// (D6) LOGOUT ENDPOINT
app.post("/out", (req, res) => {
  res.clearCookie("JWT");
  res.status(200);
  // res.send("OK");
  res.redirect('/')
});

//end

app.use(
  cors({
    origin: '<http://localhost:3000>', //or whatever your frontend server is!
    credentials: true,
  })
);

// SSX Begins

app.use(SSXExpressMiddleware(ssx));

app.get('/', (req: Request, res: Response) => {
  res.send('Express + TypeScript Server');
});

app.get('/userdata', async (req: Request, res: Response) => {
  //the below code will return a false message if the user isn't logged in via either method.
  if ((!req.ssx.verified && !jwtVerify(req.cookies)))  {
    return res.status(401).json({ success: false, message: 'Unauthorized' });
  }

  //if success message = true for either web3 or web2, load this:
  const data = await getDataFromNode(req.ssx.siwe?.address);

  res.json({
    success: true,
    userId: req.ssx.siwe?.address,
    access: 'Authorized!',
    message: 'Some user data, retrieved from a blockchain node the server can access.',
    ...data,
  });
});

app.use((req, res) => {
  if (!res.headersSent) {
    res.status(404).json({ message: 'Invalid API route', success: false });
  }
});

app.listen(port, () => {
  console.log(`⚡️[server]: Server is running at <http://localhost>:${port}`);
});

async function getDataFromNode(address: string | undefined) {
  if (!address) {
    return {};
  }
  const balanceRaw = await ssx.provider.getBalance(address);
  const balance = utils.formatEther(balanceRaw);
  const currentBlock = await ssx.provider.getBlockNumber();
  return { balance, currentBlock };
}

Running tests. while still in your server directory. Open your terminal and run:

npm run dev

To start up your server. It should be live on http://localhost:5001. Visit the ‘/userdata’ endpoint. You should be met with this screen:

That’s because you haven’t been authenticated yet. Go back into your client directory, and run:

npm start

All things considered, you should see this:

Now sign in with your Ethereum wallet and reload: http://localhost:5001/userdata, you should see something like this:

Conclusion

First off, congratulations! You’ve successfully integrated SSX into your dapp! With this integration, you can do REALLY cool things like displaying the list of NFTs in a wallet. Enables payments directly from your ‘web2 ’dapp’ ’ - I totally made that up! You can also gain insights into users’ behavior on your web app and make necessary improvements! Happy coding!

Links:

Fully Functional Express React App - No SSX Integration

Fully Functional SSX Express React App

Developer Dao SSX Article

Spruce

Geniusyinka twitter

Geniusyinka Youtube