reown-com/reown_flutter

Assistance with SIWE

Opened this issue ยท 19 comments

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

Hello @rhamnett, here you can see how we constructed our SIWE service example https://github.com/reown-com/reown_flutter/blob/develop/packages/reown_appkit/example/modal/lib/services/siwe_service.dart

/auth/v1/authenticate endpoint is being called during siweService.verifyMessage(); which is being called here https://github.com/reown-com/reown_flutter/blob/develop/packages/reown_appkit/example/modal/lib/home_page.dart#L125

But this is just how we constructed it for explanatory purposes, you don't necessarily need to follow our way. Essentially the SIWEConfig we provide it's just "glue" between AppKit and your backend service but your backend service can be whatever you want

Thanks so much again for your response.

I have copied the entire siwe service and I can see a successful request for a nonce but I never get a call to the verify/auth. I must be missing something.

Because you are missing a bunch of --dart-define variable that we run on our side. Again, the purpose of that SIWE service is just explanatory.

OK thanks I'll figure it out.

@quetool in the original post I did mention that I provide the --dart-define="AUTH_SERVICE_URL=https://2264vhbgqg.execute-api.eu-west-1.amazonaws.com" in my flutter run command and I can see that I'm hitting the nonce generation server side, and correctly receiving the nonce and token in the SIWE Service.....just the auth never gets called.

I can't see any other defines that I might be missing, so just curious as to any pointers?

Any chance you can share your project so I can clone and run?

Any chance you can share your project so I can clone and run?

Yes it was in the original post instructions:

https://github.com/rhamnett/reown_flutter

Yes, sorry, allow me some time

No problem at all, please take your time - appreciate any support.

Hello @rhamnett, I do see verifyMessage() (therefor /auth/v1/authenticate endpoint) getting called:

Screenshot 2024-11-21 at 16 06 38

Replace your _initializeService() with this one and try again

void _initializeService(_) async {
  ReownAppKitModalNetworks.removeTestNetworks();
  ReownAppKitModalNetworks.removeSupportedNetworks('solana');

  // Add this network as the first entry
  final etherlink = ReownAppKitModalNetworkInfo(
    name: 'Etherlink',
    chainId: '42793',
    currency: 'XTZ',
    rpcUrl: 'https://node.mainnet.etherlink.com',
    explorerUrl: 'https://etherlink.io',
    chainIcon: 'https://cryptologos.cc/logos/tezos-xtz-logo.png',
    isTestNetwork: false,
  );

  ReownAppKitModalNetworks.addSupportedNetworks('eip155', [etherlink]);

  try {
    _appKitModal = ReownAppKitModal(
      context: context,
      projectId: DartDefines.projectId,
      logLevel: LogLevel.all,
      metadata: _pairingMetadata(),
      siweConfig: _siweConfig(true),
      enableAnalytics: true, // OPTIONAL - null by default
      includedWalletIds: {},
      featuredWalletIds: {
        'f71e9b2c658264f7c6dfe938bbf9d2a025acc7ba4245eea2356e2995b1fd24d3', // m1nty
        'c57ca95b47569778a828d19178114f4db188b89b763c899ba0be274e97267d96', // Metamask
      },
    );

    overlay = OverlayController(
      const Duration(milliseconds: 200),
      appKitModal: _appKitModal,
    );

    _toggleOverlay();

    setState(() => _initialized = true);
  } on ReownAppKitModalException catch (e) {
    debugPrint('โ›”๏ธ ${e.message}');
    return;
  }
  // modal specific subscriptions
  _appKitModal.onModalConnect.subscribe(_onModalConnect);
  _appKitModal.onModalUpdate.subscribe(_onModalUpdate);
  _appKitModal.onModalNetworkChange.subscribe(_onModalNetworkChange);
  _appKitModal.onModalDisconnect.subscribe(_onModalDisconnect);
  _appKitModal.onModalError.subscribe(_onModalError);
  // session related subscriptions
  _appKitModal.onSessionExpireEvent.subscribe(_onSessionExpired);
  _appKitModal.onSessionUpdateEvent.subscribe(_onSessionUpdate);
  _appKitModal.onSessionEventEvent.subscribe(_onSessionEvent);

  // relayClient subscriptions
  _appKitModal.appKit!.core.relayClient.onRelayClientConnect.subscribe(
    _onRelayClientConnect,
  );
  _appKitModal.appKit!.core.relayClient.onRelayClientError.subscribe(
    _onRelayClientError,
  );
  _appKitModal.appKit!.core.relayClient.onRelayClientDisconnect.subscribe(
    _onRelayClientDisconnect,
  );
  // _appKitModal.appKit!.core.addLogListener(_logListener);

  //
  await _appKitModal.init();

  DeepLinkHandler.init(_appKitModal);
  DeepLinkHandler.checkInitialLink();

  setState(() {});
}

@quetool Thanks again - I can see your verify attempts in my backend logs.

The issue appears to be when I am using social logins, can you try with apple signin? i dont see any verify request in the backend....it hangs.

Is social features working for you or do you see any errors when loading appkit?

if i turn off swe then i can log in fine with Apple,

when turning on SWIE i get it hanging after it's successfully got the nonce from the server, pls see my original post for the error log :)

Hello @rhamnett! Can you add this somewhere in your widget tree?

AppKitModalAccountButton(appKitModal: appKit, custom: const SizedBox.shrink()),

Hello @rhamnett! Can you add this somewhere in your widget tree?

AppKitModalAccountButton(appKitModal: appKit, custom: const SizedBox.shrink()),

Sure. Do you want me to replace the existing or add this as well as?

Add this in a part of the widget tree that doesn't get disposed

@quetool that works, thanks again. Is this something I have simply done wrong or is there a fix required?

Probably something that we can do better on our side. We'll take a look in the coming days and let you know.

Thanks! No rush here, no blockers.

Will re-open to keep it on the list.