Authentication Flow
Complete authentication implementation guide for XRPL wallet integration with XRPL.Sale platform, including code examples and security best practices.
XRPL Wallet Authentication Overview
XRPL.Sale uses cryptographic signature verification for authentication, leveraging XRPL's native security model. Users sign messages with their wallet to prove ownership without exposing private keys.
Signature-Based
No passwords or API keys required
Secure
Private keys never leave the wallet
Standards-Based
Compatible with all XRPL wallets
Authentication Flow Diagram
Wallet Connect
User initiates connection
Challenge
Platform generates nonce
Sign
Wallet signs message
Verify
Platform verifies signature
Implementation Guide
Frontend Implementation (JavaScript)
1. Xaman Wallet Integration
Installation
npm install xumm-sdk
// or
<script src="https://cdn.jsdelivr.net/npm/xumm-sdk@latest/dist/xumm.min.js"></script>
Authentication Code
import { XummPkce } from 'xumm-oauth2-pkce';
class XRPLAuthentication {
constructor() {
this.xumm = new XummPkce('your-app-key');
this.userToken = null;
}
async connectWallet() {
try {
// Authorize with Xaman
const authorized = await this.xumm.authorize();
if (authorized) {
this.userToken = authorized;
const account = await this.xumm.user.account;
return await this.authenticateWithPlatform(account);
}
} catch (error) {
console.error('Wallet connection failed:', error);
throw error;
}
}
async authenticateWithPlatform(account) {
// Request authentication challenge
const challenge = await this.requestChallenge(account);
// Sign the challenge
const signature = await this.signChallenge(challenge);
// Verify with platform
return await this.verifySignature(account, challenge, signature);
}
async requestChallenge(account) {
const response = await fetch('/api/auth/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ account })
});
const data = await response.json();
return data.challenge;
}
async signChallenge(challenge) {
const payload = {
TransactionType: 'SignIn',
Message: challenge,
Account: this.userToken.account
};
const request = await this.xumm.payload.createAndSubscribe(payload);
const result = await request.resolved;
if (result.signed) {
return result.signature;
} else {
throw new Error('User cancelled signing');
}
}
async verifySignature(account, challenge, signature) {
const response = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ account, challenge, signature })
});
return await response.json();
}
}
2. Crossmark Wallet Integration
Crossmark Authentication
class CrossmarkAuth {
async connectWallet() {
try {
// Check if Crossmark is installed
if (!window.crossmark) {
throw new Error('Crossmark extension not found');
}
// Request connection
const response = await crossmark.methods.signInAndWait();
if (response.response.data.address) {
const account = response.response.data.address;
return await this.authenticateWithPlatform(account);
}
} catch (error) {
console.error('Crossmark connection failed:', error);
throw error;
}
}
async authenticateWithPlatform(account) {
// Get challenge from platform
const challenge = await this.requestChallenge(account);
// Sign with Crossmark
const message = `XRPL.Sale Authentication: ${challenge}`;
const signResponse = await crossmark.methods.signMessage({
message: message
});
if (signResponse.response.data.signature) {
return await this.verifySignature(
account,
challenge,
signResponse.response.data.signature
);
}
}
// ... rest of implementation similar to Xaman
}
3. Generic XRPL Library Integration
Using xrpl.js Library
import xrpl from 'xrpl';
class XRPLAuth {
constructor() {
this.client = new xrpl.Client('wss://xrplcluster.com/');
}
async authenticateWithWallet(wallet) {
try {
await this.client.connect();
// Get challenge from platform
const challenge = await this.requestChallenge(wallet.address);
// Create signature message
const message = this.createSignatureMessage(challenge);
// Sign the message
const signature = wallet.sign(message);
// Verify with platform
return await this.verifySignature(
wallet.address,
challenge,
signature
);
} finally {
await this.client.disconnect();
}
}
createSignatureMessage(challenge) {
return {
TransactionType: 'SignIn',
Account: wallet.address,
Message: challenge,
Sequence: 0,
Fee: '0',
LastLedgerSequence: 0
};
}
async signMessage(wallet, message) {
const encodedMessage = xrpl.encode(message);
return wallet.sign(encodedMessage);
}
}
Backend Implementation
Python Flask Implementation
Authentication Routes
from flask import Flask, request, jsonify
from flask_login import login_user, login_required
import xrpl
import secrets
import hashlib
import time
import redis
app = Flask(__name__)
redis_client = redis.Redis(host='localhost', port=6379, db=0)
class XRPLAuthService:
def __init__(self):
self.client = xrpl.Client("wss://xrplcluster.com/")
def generate_challenge(self, account):
"""Generate a unique challenge for the account"""
timestamp = int(time.time())
nonce = secrets.token_hex(16)
challenge = f"XRPL.Sale-{account}-{timestamp}-{nonce}"
# Store challenge in Redis with 5 minute expiration
redis_client.setex(f"auth_challenge:{account}", 300, challenge)
return challenge
def verify_signature(self, account, challenge, signature):
"""Verify the signature against the challenge"""
try:
# Check if challenge is valid and not expired
stored_challenge = redis_client.get(f"auth_challenge:{account}")
if not stored_challenge or stored_challenge.decode() != challenge:
return False, "Invalid or expired challenge"
# Verify signature using XRPL library
message = self.create_sign_message(account, challenge)
is_valid = xrpl.utils.verify(message, signature, account)
if is_valid:
# Clean up challenge
redis_client.delete(f"auth_challenge:{account}")
return True, "Authentication successful"
else:
return False, "Invalid signature"
except Exception as e:
return False, f"Verification error: {str(e)}"
def create_sign_message(self, account, challenge):
"""Create the message that should be signed"""
return {
"TransactionType": "SignIn",
"Account": account,
"Message": challenge,
"Sequence": 0,
"Fee": "0"
}
auth_service = XRPLAuthService()
@app.route('/api/auth/challenge', methods=['POST'])
def request_challenge():
"""Generate authentication challenge"""
data = request.get_json()
account = data.get('account')
if not account or not xrpl.utils.is_valid_xaddress(account):
return jsonify({"error": "Invalid account address"}), 400
challenge = auth_service.generate_challenge(account)
return jsonify({"challenge": challenge})
@app.route('/api/auth/verify', methods=['POST'])
def verify_authentication():
"""Verify signature and authenticate user"""
data = request.get_json()
account = data.get('account')
challenge = data.get('challenge')
signature = data.get('signature')
if not all([account, challenge, signature]):
return jsonify({"error": "Missing required fields"}), 400
is_valid, message = auth_service.verify_signature(account, challenge, signature)
if is_valid:
# Create or get user
user = User.get_or_create(account)
# Log in user
login_user(user)
return jsonify({
"success": True,
"message": message,
"user": {
"account": account,
"authenticated": True
}
})
else:
return jsonify({
"success": False,
"message": message
}), 401
Security Best Practices
Challenge Generation
Signature Verification
Session Management
Session Creation
def create_user_session(account):
"""Create authenticated user session"""
session_id = secrets.token_hex(32)
session_data = {
'account': account,
'authenticated': True,
'created_at': int(time.time()),
'last_activity': int(time.time())
}
# Store session with 24 hour expiration
redis_client.setex(
f"session:{session_id}",
86400,
json.dumps(session_data)
)
return session_id
Session Validation
def validate_session(session_id):
"""Validate user session"""
try:
session_data = redis_client.get(f"session:{session_id}")
if not session_data:
return None
session = json.loads(session_data)
# Update last activity
session['last_activity'] = int(time.time())
redis_client.setex(
f"session:{session_id}",
86400,
json.dumps(session)
)
return session
except Exception:
return None
Error Handling & Edge Cases
Common Error Scenarios
Client-Side Errors
- • Wallet extension not installed
- • User rejects signing request
- • Network connection issues
- • Invalid wallet state
- • Wallet locked or unavailable
Server-Side Errors
- • Challenge generation failure
- • Signature verification failure
- • Session creation errors
- • Database connectivity issues
- • XRPL network connectivity
Error Handling Implementation
class AuthenticationHandler {
async handleAuthenticationError(error) {
let userMessage = "Authentication failed. Please try again.";
let shouldRetry = false;
switch (error.type) {
case 'WALLET_NOT_FOUND':
userMessage = "Please install a compatible XRPL wallet (Xaman or Crossmark).";
break;
case 'USER_REJECTED':
userMessage = "Authentication cancelled by user.";
break;
case 'NETWORK_ERROR':
userMessage = "Network connection error. Please check your connection.";
shouldRetry = true;
break;
case 'CHALLENGE_EXPIRED':
userMessage = "Authentication session expired. Please try again.";
shouldRetry = true;
break;
case 'INVALID_SIGNATURE':
userMessage = "Signature verification failed. Please ensure your wallet is unlocked.";
shouldRetry = true;
break;
default:
console.error('Unexpected authentication error:', error);
}
return { message: userMessage, canRetry: shouldRetry };
}
}
Testing Authentication Flow
Test Account Setup
// Create test wallet for development
import xrpl from 'xrpl';
const testWallet = xrpl.Wallet.generate();
console.log("Test Wallet:");
console.log("Address:", testWallet.address);
console.log("Public Key:", testWallet.publicKey);
// Never log private key in production!
// Fund wallet on testnet
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233/');
await client.connect();
await client.fundWallet(testWallet);
Unit Test Example
// Jest test for authentication
describe('XRPL Authentication', () => {
let authService;
let testWallet;
beforeEach(() => {
authService = new XRPLAuthService();
testWallet = xrpl.Wallet.generate();
});
test('should generate valid challenge', () => {
const challenge = authService.generate_challenge(
testWallet.address
);
expect(challenge).toMatch(/^XRPL\.Sale-r[a-zA-Z0-9]+/);
});
test('should verify valid signature', async () => {
const challenge = authService.generate_challenge(
testWallet.address
);
const message = authService.create_sign_message(
testWallet.address,
challenge
);
const signature = testWallet.sign(message);
const [isValid, message] = authService.verify_signature(
testWallet.address,
challenge,
signature
);
expect(isValid).toBe(true);
});
});