Security
Best Practices

Security Best Practices

Essential security guidelines for integrating Veridex passkey wallets.

Passkey Security

Credential Storage

// ✅ Good: Let the browser/OS manage credentials
await sdk.passkey.register('user@example.com', 'My Wallet');
// Credential stored securely in platform authenticator
 
// ❌ Bad: Don't try to extract or store private keys
// The private key never leaves the secure enclave

User Verification

// ✅ Require user verification for sensitive operations
await sdk.passkey.authenticate({ userVerification: 'required' });
 
// The platform will require biometric/PIN verification

Credential Binding

// Bind credentials to your domain
const credential = await sdk.passkey.register('user@example.com', 'Wallet', {
  // Only your domain can use this credential
  rp: {
    id: 'yourdomain.com',
    name: 'Your App',
  },
});

Cross-Origin Authentication Security

When integrating cross-domain passkeys, follow these additional security practices.

Register Your Production Origin

// ✅ Register your exact production origin
// POST /api/v1/apps/register
// { "name": "My App", "origin": "https://myapp.com" }
 
// ❌ Don't rely on wildcard or unregistered origins
// Unregistered origins are blocked by the Auth Portal

Validate Sessions Server-Side

// ✅ Good: Validate server session tokens on your backend
async function protectedEndpoint(req: Request) {
  const sessionId = req.headers.get('X-Veridex-Session');
  
  const response = await fetch(
    `https://relayer.veridex.network/api/v1/session/${sessionId}`
  );
  const data = await response.json();
  
  if (!data.valid) {
    return new Response('Unauthorized', { status: 401 });
  }
  
  // Verify the session belongs to the expected origin
  if (data.session.appOrigin !== 'https://myapp.com') {
    return new Response('Origin mismatch', { status: 403 });
  }
  
  // Proceed with the request
}
 
// ❌ Bad: Trust client-side session data without verification
// const session = JSON.parse(localStorage.getItem('veridex_session'));
// if (session) { grantAccess(); } // INSECURE

Protect Your API Key

// ✅ Store API key server-side only
// Use environment variables, never client-side code
const apiKey = process.env.VERIDEX_API_KEY; // Server only
 
// ❌ Never expose in client-side code
// const apiKey = 'vdx_sk_...'; // EXPOSED TO USERS

Revoke Sessions on Logout

// ✅ Always revoke server sessions when users log out
async function logout(serverSessionId: string) {
  await fetch(
    `https://relayer.veridex.network/api/v1/session/${serverSessionId}`,
    { method: 'DELETE' }
  );
  localStorage.removeItem('veridex_session');
}

Validate the Auth Portal Origin

// ✅ When listening for postMessage from the Auth Portal,
// always verify the origin
window.addEventListener('message', (event) => {
  // Only accept messages from the Veridex Auth Portal
  if (!event.origin.includes('veridex.network')) {
    return; // Ignore messages from unknown origins
  }
  
  // Process the auth response
  const { type, payload } = event.data;
  // ...
});

Use Short-Lived Sessions

// ✅ Good: Short-lived sessions with minimal permissions
const { serverSession } = await auth.authenticateAndCreateSession({
  permissions: ['read'],        // Only what you need
  expiresInMs: 1800000,         // 30 minutes
});
 
// ❌ Bad: Long-lived sessions with broad permissions
const { serverSession } = await auth.authenticateAndCreateSession({
  permissions: ['read', 'transfer', 'admin'],
  expiresInMs: 86400000 * 30,   // 30 days
});

Session Key Security

Limit Session Scope

// ✅ Good: Minimal permissions
const session = await sdk.sessions.create({
  duration: 1800, // 30 minutes, not days
  maxValue: parseUnits('100', 6), // $100 max, not unlimited
  allowedTokens: [USDC], // Only needed tokens
  allowedContracts: [SPECIFIC_CONTRACT], // Only needed contracts
});
 
// ❌ Bad: Overly permissive
const session = await sdk.sessions.create({
  duration: 86400 * 30, // 30 days
  maxValue: parseUnits('1000000', 6), // $1M max
  // No token/contract restrictions
});

Session Expiry Handling

function isSessionValid(): boolean {
  const session = sdk.sessions.getActive();
  if (!session) return false;
  
  // Add buffer for network latency
  const buffer = 60 * 1000; // 1 minute
  return session.expiresAt > Date.now() + buffer;
}
 
async function ensureValidSession() {
  if (!isSessionValid()) {
    await sdk.sessions.create({ duration: 1800 });
  }
}

Revoke Sessions

// Revoke session on logout
async function logout() {
  await sdk.sessions.revoke();
  sdk.passkey.clearCredential();
}
 
// Revoke session on suspicious activity
async function handleSuspiciousActivity() {
  await sdk.sessions.revoke();
  notifyUser('Session terminated for security');
}

Transaction Security

Validate Recipients

// ✅ Validate addresses before sending
function isValidAddress(address: string): boolean {
  return ethers.isAddress(address);
}
 
async function safeSend(recipient: string, amount: bigint) {
  if (!isValidAddress(recipient)) {
    throw new Error('Invalid recipient address');
  }
  
  // Additional checks
  if (recipient === ethers.ZeroAddress) {
    throw new Error('Cannot send to zero address');
  }
  
  return await sdk.transferViaRelayer({ token, recipient, amount });
}

Amount Validation

// ✅ Validate amounts
async function validateTransfer(amount: bigint, token: string) {
  const balance = await sdk.getBalance(token);
  
  if (amount <= 0n) {
    throw new Error('Amount must be positive');
  }
  
  if (amount > balance) {
    throw new Error('Insufficient balance');
  }
  
  // Check against user's spending limit
  const limit = await getUserSpendingLimit();
  if (amount > limit) {
    throw new Error('Exceeds spending limit');
  }
}

Transaction Signing

// ✅ Always verify what you're signing
async function signTransaction(tx: TransactionRequest) {
  // Show user what they're signing
  const confirmed = await showConfirmationDialog({
    to: tx.to,
    value: tx.value,
    data: tx.data,
  });
  
  if (!confirmed) {
    throw new Error('User rejected transaction');
  }
  
  return await sdk.signAndSend(tx);
}

Contract Interaction Security

Verify Contract Addresses

// ✅ Maintain allowlist of known contracts
const VERIFIED_CONTRACTS = {
  uniswap: '0x2626664c2603336E57B271c5C0b26F421741e481',
  aave: '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5',
};
 
async function safeContractCall(contract: string, data: string) {
  const isVerified = Object.values(VERIFIED_CONTRACTS).includes(contract);
  
  if (!isVerified) {
    // Require additional confirmation for unknown contracts
    const confirmed = await confirmUnknownContract(contract);
    if (!confirmed) throw new Error('Rejected unknown contract');
  }
  
  return await sdk.executeViaRelayer({ target: contract, data });
}

Decode Transaction Data

// ✅ Show users decoded transaction data
import { Interface } from 'ethers';
 
function decodeTransaction(to: string, data: string) {
  const knownInterfaces = {
    erc20: new Interface([
      'function transfer(address to, uint256 amount)',
      'function approve(address spender, uint256 amount)',
    ]),
    // Add more known interfaces
  };
  
  for (const [name, iface] of Object.entries(knownInterfaces)) {
    try {
      const decoded = iface.parseTransaction({ data });
      return {
        contract: name,
        method: decoded.name,
        args: decoded.args,
      };
    } catch {
      continue;
    }
  }
  
  return { contract: 'unknown', method: 'unknown', args: [] };
}

API Security

Secure Relayer Communication

// ✅ Always use HTTPS
const sdk = createSDK('base', {
  relayerUrl: 'https://relayer.veridex.network', // HTTPS only
});
 
// ❌ Never use HTTP in production
// relayerUrl: 'http://...' // INSECURE

Rate Limiting

// Implement client-side rate limiting
class RateLimiter {
  private requests: number[] = [];
  private limit: number;
  private window: number;
  
  constructor(limit: number, windowMs: number) {
    this.limit = limit;
    this.window = windowMs;
  }
  
  canRequest(): boolean {
    const now = Date.now();
    this.requests = this.requests.filter(t => t > now - this.window);
    return this.requests.length < this.limit;
  }
  
  recordRequest() {
    this.requests.push(Date.now());
  }
}
 
const limiter = new RateLimiter(10, 60000); // 10 requests per minute
 
async function rateLimitedCall(fn: () => Promise<any>) {
  if (!limiter.canRequest()) {
    throw new Error('Rate limit exceeded');
  }
  limiter.recordRequest();
  return await fn();
}

Error Handling

Don't Expose Sensitive Information

// ✅ Safe error handling
try {
  await sdk.transferViaRelayer({ token, recipient, amount });
} catch (error) {
  // Log full error internally
  console.error('Transfer failed:', error);
  
  // Show generic message to user
  showError('Transfer failed. Please try again.');
}
 
// ❌ Don't expose internal errors
// showError(error.message); // May contain sensitive info

Handle Authentication Errors

async function handleAuthError(error: Error) {
  if (error.message.includes('NotAllowedError')) {
    // User cancelled or timeout
    showMessage('Authentication cancelled');
  } else if (error.message.includes('InvalidStateError')) {
    // Credential not found
    showMessage('Please register a passkey first');
  } else if (error.message.includes('NotSupportedError')) {
    // Browser doesn't support WebAuthn
    showMessage('Your browser does not support passkeys');
  }
}

Monitoring & Logging

Audit Logging

// Log security-relevant events
function auditLog(event: string, data: object) {
  const entry = {
    timestamp: new Date().toISOString(),
    event,
    data,
    // Don't log sensitive data like credentials
  };
  
  // Send to logging service
  sendToLogger(entry);
}
 
// Usage
auditLog('session_created', { duration: 1800, maxValue: '100' });
auditLog('transfer_initiated', { to: recipient, amount: '50' });

Monitor for Anomalies

// Detect unusual activity
async function checkForAnomalies(tx: Transaction) {
  const recentTxs = await getRecentTransactions();
  
  // Check for unusual patterns
  const unusualAmount = tx.amount > averageAmount(recentTxs) * 10;
  const newRecipient = !recentTxs.some(t => t.to === tx.to);
  const highFrequency = recentTxs.length > 10;
  
  if (unusualAmount || newRecipient && highFrequency) {
    await requestAdditionalVerification();
  }
}

Checklist

Before Launch

  • All API calls use HTTPS
  • Session keys have reasonable limits
  • Error messages don't expose internals
  • Rate limiting implemented
  • Audit logging in place
  • Contract addresses verified
  • Amount validation implemented
  • User verification required for sensitive ops
  • Production origin registered at developers.veridex.network (opens in a new tab)
  • API key stored server-side only (never in client code)
  • Server-side session validation implemented
  • Sessions revoked on user logout

Ongoing

  • Monitor for unusual activity
  • Keep SDK updated
  • Review session configurations
  • Audit access logs regularly
  • Test recovery procedures
  • Review cross-origin session token expiry settings
  • Monitor app registration status via relayer API