AI Assistant

Authentication Flow

Complete authentication implementation guide for XRPL wallet integration with XRPL.Sale platform, including code examples and security best practices.

15 min read Developer Guide

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

Cryptographically Secure Randomness
Use secrets.token_hex() or equivalent
Time-based Expiration
Challenges expire after 5-10 minutes
Single Use
Each challenge can only be used once
Account Binding
Challenges are tied to specific accounts

Signature Verification

Cryptographic Verification
Use XRPL's built-in signature verification
Message Integrity
Verify complete message structure
Account Ownership
Ensure signature matches claimed account
Replay Protection
Prevent signature reuse attacks

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);
    });
});