Here we learn how to implement sessions with Express.js to add the necessary backend security.
A completed version of the updated backend can be found here (03_complete_app/backend). This example uses the browser console to print messages, so it should be actively monitored.
For additional security against replay attacks, it is not enough for the backend to generate the nonce. It must also be tied to a browser session with the end-user. In the siwe-backend directory, install the following and edit index.js:
yarn add express-session
Update src/index.js to the following:
src/index.js
import cors from'cors';import express from'express';import Session from'express-session';import { generateNonce, SiweMessage } from'siwe';constapp=express();app.use(express.json());app.use(cors({ origin:'http://localhost:8080', credentials:true,}))app.use(Session({ name:'siwe-quickstart', secret:"siwe-quickstart-secret", resave:true, saveUninitialized:true, cookie: { secure:false, sameSite:true }}));app.get('/nonce',asyncfunction (req, res) {req.session.nonce =generateNonce();res.setHeader('Content-Type','text/plain');res.status(200).send(req.session.nonce);});app.post('/verify',asyncfunction (req, res) {try {if (!req.body.message) {res.status(422).json({ message:'Expected prepareMessage object as body.' });return; }let SIWEObject =newSiweMessage(req.body.message);const { data: message } =awaitSIWEObject.verify({ signature:req.body.signature, nonce:req.session.nonce });req.session.siwe = message;req.session.cookie.expires =newDate(message.expirationTime);req.session.save(() =>res.status(200).send(true)); } catch (e) {req.session.siwe =null;req.session.nonce =null;console.error(e);switch (e) {caseErrorTypes.EXPIRED_MESSAGE: {req.session.save(() =>res.status(440).json({ message:e.message }));break; }caseErrorTypes.INVALID_SIGNATURE: {req.session.save(() =>res.status(422).json({ message:e.message }));break; }default: {req.session.save(() =>res.status(500).json({ message:e.message }));break; } } }});app.get('/personal_information',function (req, res) {if (!req.session.siwe) {res.status(401).json({ message:'You have to first sign_in' });return; }console.log("User is authenticated!");res.setHeader('Content-Type','text/plain');res.send(`You are authenticated and your address is: ${req.session.siwe.address}`);});app.listen(3000);
This way, the session (req.session) stores the nonce for the initial validation of the message, and once that's done, more can be added. For example, here we store the message's fields, so we can always reference the address of the user.
A potential extension is to resolve the ENS domain of the user and keep it in the session.
On the client side, the flow is similar to the previous example, except we now need to send cookies with our requests for the session to work. We can add a new endpoint, personal_information, to retrieve the information from the session in place, without having to send the message and signature every time.
In the siwe-frontend directory, stop any running instances and populate src/index.js to match the following:
3. For this last step, you need to have both the frontend and backend running together. Start by running the backend server with the following command from the parent directory:
cd siwe-backend
yarn start
In a separate terminal, start the frontend by running the following command and visit the URL printed to the console:
cd siwe-frontend
yarn start
4. Try to Sign-In with Ethereum by visiting the URL printed to the console, connecting your wallet, and signing in. You can now hit the Get session information button to receive a message similar to the following in the console:
You are authenticated and your address is:
0x6Ee9894c677EFa1c56392e5E7533DE76004C8D94