Authentication with Message Hashing

Use DropFi Wallet message signing to authenticate users without passwords. Prove wallet ownership via signed messages and issue sessions on your backend.

Overview

Authentication with Message Hashing

Sign-in with wallet using message signing

You can use the XRPL Injection API's message signing feature to authenticate users. Instead of passwords, the user signs a challenge message with their wallet. Your backend verifies the signature and derives the wallet address to create a session. This is often called "Sign-in with wallet" or "wallet-based authentication."

No Passwords

Users prove ownership by signing a message with their wallet

Replay Protection

Include a timestamp in the message and reject expired signatures

Cryptographic Verification

Verify signature with public key and derive address on the server

Session Issuance

Issue JWT or session cookie after successful verification

Authentication Flow

The flow has two sides: the client (your DApp) and the server (your API).
1

Connect wallet

Use the Injection API to connect and get the wallet address

Ensure window.xrpl is available and call connect (e.g. connectXrpl or equivalent).

2

Build the message

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).

3

Sign the message

Ask the wallet to sign the message

Call signMessage(message). The wallet returns signature and publicKey.

4

Send to your backend

POST message, publicKey, and signature to your auth endpoint

Send credentials (e.g. with cookies enabled if you use session cookies).

5

Verify and create session

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.

Client-Side (DApp)

On the client, connect the wallet, build a message that includes the wallet address and a timestamp, then sign it and send the result to your API.
Connect and sign for authentication
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
}
Use a consistent message format so your server can parse the timestamp. Including the wallet address in the message ties the signature to that address and helps prevent misuse.

Server-Side (API)

On the server, validate the timestamp, verify the signature using the public key, derive the XRPL address, then create or find the user and issue a session (e.g. JWT or cookie).
Verify signature and issue session
import { verify, deriveAddress } from 'ripple-keypairs';
import jwt from 'jsonwebtoken';
import dayjs from 'dayjs';
// POST /api/auth/sign
const { 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 key
const wallet_address = deriveAddress(publicKey);
// 4. Create or find user in your DB, then issue session
const access_token = jwt.sign(
{ wallet_address },
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: '240h' }
);
// 5. Set cookie or return token
const 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 });
The verify function from ripple-keypairs expects the message in hex. Convert the UTF-8 message to hex with Buffer.from(message, 'utf8').toString('hex'). Never skip timestamp validation in production.

API Requirements

Your auth endpoint should accept a POST body with the following fields:
POST
/api/auth/sign

Authenticate with message, publicKey, and signature

Parameters: Body: { message: string, publicKey: string, signature: string }

The client obtains signature and publicKey by calling signMessage(message) on the injected wallet (e.g. via window.xrpl or your React integration). The same message string must be sent to the server so it can verify the signature.

Best Practices

Follow these practices when implementing wallet-based authentication:
  • Always include a timestamp in the signed message and reject signatures older than a short window (e.g. 60 seconds) to prevent replay attacks.
  • Use HTTPS and secure, HttpOnly cookies (or secure token storage) for the session.
  • Derive the wallet address on the server from the public key; do not trust the client to send the correct address.
  • Use the same message string on client and server for verification; the server converts it to hex for ripple-keypairs verify().
  • Handle user rejection and missing wallet (e.g. prompt to install DropFi) gracefully in the UI.

Replay protection

A timestamp in the message and a short validity window ensure that a captured request cannot be reused later.

Message format

If you change the message format (e.g. add/remove lines), ensure both client and server use the exact same string before hex conversion.

On This Page

Select a documentation page to see its table of contents