Moving From SIWE to SSX
A tutorial on integrating SSX
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.
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.
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:- Update/verify your API calls work with the session cookie.
- Remove the SIWE library and unused functions/endpoints.
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.ts
add 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
.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.ts
add 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));
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;
}
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.
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);
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.
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 modified 3mo ago