Skip to content

Assistance with SIWE #41

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

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions