Skip to content

Assistance with SIWE #41

@rhamnett

Description

@rhamnett

Describe the bug

Hello, the documentation lacks an example of a SIWE server. I was wondering if you can kindly help me debug my implementation.

When SIWE is enabled, the wallet successfully makes a request to /auth/v1/nonce and I can see a reply, but I never see a call to /auth/v1/authenticate

To Reproduce

Steps to reproduce the behavior:

  1. git clone https://github.com/rhamnett/reown_flutter.git
  2. flutter run --dart-define="PROJECT_ID=3de10c688399aa49889ff67453c20ae4" --dart-define="AUTH_SERVICE_URL=https://2264vhbgqg.execute-api.eu-west-1.amazonaws.com"
  3. try to log in with siweAuthValue set to true - final siweAuthValue = prefs.getBool('appkit_siwe_auth') ?? true;
  4. Fails to sign

Error log:

flutter: 2024-11-21 11:18:54.937725 🐛 [SiweService] getNonce() called
flutter: [SIWEConfig] getNonce()
flutter: 2024-11-21 11:18:55.084693 📝 [AnalyticsService] send event 202: {"eventId":"7e64ea11-3854-4da0-ac93-b42936abbaf2","bundleId":"com.web3modal.flutterExample3","timestamp":1732187934936,"props":{"type":"track","event":"CLICK_SIGN_SIWE_MESSAGE","properties":{"network":"1"}}}
flutter: [SIWESERVICE] getNonce() => {"nonce":"73643","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6IjczNjQzIiwiaWF0IjoxNzMyMTg3OTM2LCJleHAiOjE3MzIxODgyMzZ9.e_y4AOzlinaFQIY5Voo55CGpRLseszNwtFHuOJJWPN0"}
flutter: [SIWEConfig] getMessageParams()
flutter: 2024-11-21 11:18:56.292061 🐛 [SiweService] createMessage() called
flutter: [SIWEConfig] createMessage()
flutter: {chainId: eip155:1, domain: appkit-lab.reown.com, nonce: 73643, uri: https://appkit-lab.reown.com/login, address: eip155:1:0x4B0321761aCfc2bdE49ece923647433B4F04Dd3E, version: 1, type: {t: eip4361}, nbf: null, exp: null, statement: Welcome to AppKit 1.0.4 for Flutter., requestId: null, resources: null, expiry: null, iat: 2024-11-21T11:18:56.291Z}
flutter: 2024-11-21 11:18:56.293713 🐛 [SiweService] formatMessage() called
flutter: 2024-11-21 11:18:56.296369 🐛 [SiweService] signMessageRequest() called
flutter: 2024-11-21 11:18:56.302327 🐛 [MagicService] postMessage({"type":"@w3m-app/RPC_REQUEST","payload":{"method":"personal_sign","params":["0x6170706b69742d6c61622e72656f776e2e636f6d2077616e747320796f7520746f207369676e20696e207769746820796f757220457468657265756d206163636f756e743a0a3078344230333231373631614366633262644534396563653932333634373433334234463034446433450a0a57656c636f6d6520746f204170704b697420312e302e3420666f7220466c75747465722e0a0a5552493a2068747470733a2f2f6170706b69742d6c61622e72656f776e2e636f6d2f6c6f67696e0a56657273696f6e3a20310a436861696e2049443a20310a4e6f6e63653a2037333634330a4973737565642041743a20323032342d31312d32315431313a31383a35362e3239315a","0x4B0321761aCfc2bdE49ece923647433B4F04Dd3E"]}})

My attempt at a SIWE server:

import express, { Request, Response } from 'express';
import { SiweMessage } from 'siwe';
import jwt from 'jsonwebtoken';
import serverlessExpress from '@vendia/serverless-express';
import dotenv from 'dotenv';
import bodyParser from 'body-parser';

// Load environment variables
dotenv.config();

// Ensure JWT_SECRET is loaded
if (!process.env.JWT_SECRET) {
  throw new Error('JWT_SECRET is not defined in the environment variables.');
}

const app = express();
app.use(bodyParser.json()); // Replaced with body-parser for compatibility

const JWT_SECRET = process.env.JWT_SECRET;
const nonces: Record<string, boolean> = {}; // Temporary in-memory nonce storage

// Generate a new nonce and a preliminary token
app.get('/auth/v1/nonce', (req: Request, res: Response): void => {
  console.log('[Nonce] Received request for new nonce');

  const nonce = Math.floor(Math.random() * 1e6).toString();
  nonces[nonce] = true;
  console.log(`[Nonce] Generated nonce: ${nonce}`);

  // Generate a temporary JWT token that includes the nonce
  const tempToken = jwt.sign(
    { nonce },
    JWT_SECRET,
    { expiresIn: '5m' } // Token valid for 5 minutes
  );
  console.log('[Nonce] Generated temporary token for nonce');

  res.json({ nonce, token: tempToken });
});

// Authenticate using SIWE
app.post('/auth/v1/authenticate', async (req: Request, res: Response): Promise<void> => {
  console.log('[Auth] Received authentication request');

  try {
    const { message, signature } = req.body;
    console.log(`[Auth] Message: ${message}`);
    console.log(`[Auth] Signature: ${signature}`);

    if (!message || !signature) {
      console.log('[Auth] Missing message or signature in request body');
      res.status(400).json({ error: 'Message and signature are required.' });
      return;
    }

    const siweMessage = new SiweMessage(message);
    const fields = await siweMessage.validate(signature);
    console.log(`[Auth] SIWE message validated. Fields: ${JSON.stringify(fields)}`);

    if (!nonces[fields.nonce]) {
      console.log(`[Auth] Invalid or expired nonce: ${fields.nonce}`);
      res.status(400).json({ error: 'Invalid or expired nonce.' });
      return;
    }

    delete nonces[fields.nonce];
    console.log(`[Auth] Nonce ${fields.nonce} deleted from storage`);

    // Generate the main authentication JWT
    const authToken = jwt.sign(
      {
        address: fields.address,
        domain: fields.domain,
        issuedAt: fields.issuedAt,
      },
      JWT_SECRET,
      { expiresIn: '1h' } // Token valid for 1 hour
    );
    console.log(`[Auth] Generated auth token for address: ${fields.address}`);

    res.json({
      token: authToken,
      address: fields.address,
      message: 'Authentication successful.',
    });
    console.log('[Auth] Authentication successful');
  } catch (error) {
    console.error(`[Auth] Authentication error: ${(error as Error).message}`);
    res.status(400).json({ error: (error as Error).message || 'An unknown error occurred.' });
  }
});

// Retrieve user details
app.get('/auth/v1/me', (req: Request, res: Response): void => {
  console.log('[User] Received request to retrieve user details');

  const authHeader = req.headers.authorization;

  if (!authHeader) {
    console.log('[User] Authorization header is missing');
    res.status(401).json({ error: 'Authorization header is missing.' });
    return;
  }

  const token = authHeader.split(' ')[1];
  console.log(`[User] Extracted token: ${token}`);

  try {
    const decoded = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
    console.log(`[User] Token decoded successfully: ${JSON.stringify(decoded)}`);
    res.json({ address: decoded.address, domain: decoded.domain });
  } catch (error) {
    console.error(`[User] Token verification failed: ${(error as Error).message}`);
    res.status(401).json({ error: (error as Error).message || 'Invalid token.' });
  }
});

// Update user details
app.post('/auth/v1/update-user', (req: Request, res: Response): void => {
  console.log('[User] Received request to update user');

  const authHeader = req.headers.authorization;

  if (!authHeader) {
    console.log('[User] Authorization header is missing');
    res.status(401).json({ error: 'Authorization header is missing.' });
    return;
  }

  const token = authHeader.split(' ')[1];
  console.log(`[User] Extracted token: ${token}`);

  try {
    const decoded = jwt.verify(token, JWT_SECRET) as jwt.JwtPayload;
    const userAddress = decoded.address;
    console.log(`[User] Token decoded successfully. User address: ${userAddress}`);

    // Here you would update the user in your database.
    const { metadata } = req.body;
    console.log(`[User] Received metadata for update: ${JSON.stringify(metadata)}`);
    // Update user metadata in the database associated with userAddress

    res.status(200).json({ message: 'User updated successfully.' });
    console.log('[User] User updated successfully');
  } catch (error) {
    console.error(`[User] Token verification failed: ${(error as Error).message}`);
    res.status(401).json({ error: (error as Error).message || 'Invalid token.' });
  }
});

// Sign out
app.post('/auth/v1/sign-out', (req: Request, res: Response): void => {
  console.log('[Auth] Received sign-out request');
  // Implement any necessary sign-out logic here
  res.status(200).json({ message: 'Signed out successfully.' });
  console.log('[Auth] User signed out successfully');
});

Expected behavior
/auth/v1/authenticate endpoint gets called

Metadata

Metadata

Assignees

Labels

help wantedExtra attention is needed

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions