Table of contents
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:
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