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