Use DropFi Wallet message signing to authenticate users without passwords. Prove wallet ownership via signed messages and issue sessions on your backend.
Sign-in with wallet using message signing
Users prove ownership by signing a message with their wallet
Include a timestamp in the message and reject expired signatures
Verify signature with public key and derive address on the server
Issue JWT or session cookie after successful verification
Use the Injection API to connect and get the wallet address
Ensure window.xrpl is available and call connect (e.g. connectXrpl or equivalent).
Create a message that includes the address and a timestamp
Include a line like "Timestamp: <ISO string>" so the server can reject old signatures (e.g. older than 60 seconds).
Ask the wallet to sign the message
Call signMessage(message). The wallet returns signature and publicKey.
POST message, publicKey, and signature to your auth endpoint
Send credentials (e.g. with cookies enabled if you use session cookies).
Server verifies signature and timestamp, derives address, then issues a session
Use ripple-keypairs to verify the signature and derive the XRPL address from the public key. Create or find the user and return a JWT or set a session cookie.
async function handleConnectAndSignIn() {if (!window.xrpl) {window.open('https://dropfi.app', '_blank');return;}const address = await connectXrpl?.();if (!address) throw new Error('User canceled request');const message = `Welcome to My App!\nSign this message to prove ownership:\n\nWallet: ${address}\nTimestamp: ${new Date().toISOString()}`;const { signature, publicKey } = await signMessage?.(message);if (!signature || !publicKey) throw new Error('User rejected message');await fetch('/api/auth/sign', {method: 'POST',headers: { 'Content-Type': 'application/json' },credentials: 'include',body: JSON.stringify({ message, publicKey, signature }),});// Then refetch user state or redirect}
import { verify, deriveAddress } from 'ripple-keypairs';import jwt from 'jsonwebtoken';import dayjs from 'dayjs';// POST /api/auth/signconst { message, publicKey, signature } = await request.json();// 1. Parse and validate timestamp (replay protection)const timestampLine = message.split('\n').find((line) => line.startsWith('Timestamp:'));const timestampStr = timestampLine?.replace('Timestamp:', '').trim();if (!timestampStr || !dayjs(timestampStr).isValid()) {throw new Error('Invalid timestamp');}const MAX_AGE_SECONDS = 60;const age = dayjs().diff(dayjs(timestampStr), 'second');if (age > MAX_AGE_SECONDS) {throw new Error('Signature expired');}// 2. Verify signature (message must be UTF-8, then hex for verify)const messageHex = Buffer.from(message, 'utf8').toString('hex');if (!verify(messageHex, signature, publicKey)) {throw new Error('Invalid signature');}// 3. Derive wallet address from public keyconst wallet_address = deriveAddress(publicKey);// 4. Create or find user in your DB, then issue sessionconst access_token = jwt.sign({ wallet_address },process.env.ACCESS_TOKEN_SECRET,{ expiresIn: '240h' });// 5. Set cookie or return tokenconst headers = new Headers();headers.append('Set-Cookie',`session=${access_token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${60 * 60 * 24 * 10}`);return new Response(JSON.stringify({ success: true }), { status: 200, headers });
/api/auth/signAuthenticate with message, publicKey, and signature
Parameters: Body: { message: string, publicKey: string, signature: string }
A timestamp in the message and a short validity window ensure that a captured request cannot be reused later.
If you change the message format (e.g. add/remove lines), ensure both client and server use the exact same string before hex conversion.