Skip to Content
Welcome to RitoSwap's documentation!

SIWE Library

The SIWE (Sign-In with Ethereum) library provides RitoSwap’s authentication infrastructure for token-gated access. This internal library handles secure nonce generation, message formatting, and signature verification to ensure only legitimate token holders can access restricted content.

Overview

RitoSwap’s token gate system requires users to prove ownership of a Colored Key NFT before accessing exclusive content. The SIWE library implements this verification through cryptographic signatures, eliminating the need for traditional authentication while maintaining security.

How It Works in RitoSwap

The library operates in two main contexts within the dApp:

  1. Gate Access Flow - When users attempt to unlock the token gate
  2. Content Submission Flow - When users submit messages within the gated area

Both flows rely on the same underlying SIWE infrastructure but implement different verification strategies based on the action being performed.

      • siwe.server.ts
      • siwe.client.ts

Server Implementation

The server-side component (siwe.server.ts) provides the core security functionality for the SIWE system.

Redis Initialization

Both the SIWE and rate limiting libraries share the same Redis client initialization:

// siwe.server.ts import { Redis } from '@upstash/redis' // Initialize Redis client when enabled const redis = isSiweEnabled() ? new Redis({ url: process.env.KV_REST_API_URL!, token: process.env.KV_REST_API_TOKEN!, }) : null

This Redis instance is used for nonce storage and retrieval throughout the SIWE flow.

Configuration Check

The library includes an intelligent activation system that ensures all required infrastructure is in place:

export const isSiweEnabled = (): boolean => { const flagEnabled = process.env.NEXT_PUBLIC_ACTIVATE_REDIS === 'true' const hasApi = !!process.env.KV_REST_API_URL && process.env.KV_REST_API_URL !== 'false' && process.env.KV_REST_API_URL !== '' const hasKey = !!process.env.KV_REST_API_TOKEN && process.env.KV_REST_API_TOKEN !== 'false' && process.env.KV_REST_API_TOKEN !== '' return flagEnabled && hasApi && hasKey }

This triple-check ensures that:

  • The feature flag is explicitly enabled
  • A valid Redis URL is configured (not just present but actually valid)
  • A valid Redis token exists

Domain Resolution

The domain resolution system handles multiple deployment scenarios to prevent domain spoofing attacks:

export const getDomain = (requestHeaders?: Headers): string => { // Priority 1: Explicit configuration if (process.env.NEXT_PUBLIC_DOMAIN && process.env.NEXT_PUBLIC_DOMAIN !== 'false' && process.env.NEXT_PUBLIC_DOMAIN !== '') { return process.env.NEXT_PUBLIC_DOMAIN.replace(/^https?:\/\//, '') } // Priority 2: Request headers if (requestHeaders) { const host = requestHeaders.get('host') if (host) return host } // Priority 3: Vercel URL if (process.env.NEXT_PUBLIC_VERCEL_URL) { return process.env.NEXT_PUBLIC_VERCEL_URL } return 'localhost:3000' }

Nonce Management

The nonce system prevents replay attacks by ensuring each authentication attempt uses a unique, time-limited value:

export async function generateNonce(identifier: string): Promise<string> { const nonce = generateRandomNonce() if (redis) { await redis.setex(`nonce:${identifier}`, 300, nonce) // 5-minute TTL } return nonce } export async function verifyNonce( identifier: string, nonce: string ): Promise<boolean> { if (!redis) return true // Graceful degradation const storedNonce = await redis.get(`nonce:${identifier}`) if (storedNonce === nonce) { await redis.del(`nonce:${identifier}`) // Consume nonce return true } return false }

Key security features:

  • Nonces expire after 5 minutes
  • Each nonce can only be used once (deleted after verification)
  • Falls back gracefully when Redis is unavailable

Message Verification

The verifySiweMessage function implements comprehensive security checks:

export async function verifySiweMessage(params: { message: string signature: string nonce: string address: string requestHeaders?: Headers }): Promise<{ success: boolean; error?: string }> { // 1. Parse the SIWE message const parsed = parseSiweMessage(params.message) if (!parsed) { return { success: false, error: 'Invalid message format' } } // 2. Verify domain matches const expectedDomain = getDomain(params.requestHeaders) if (parsed.domain !== expectedDomain) { return { success: false, error: 'Domain mismatch' } } // 3. Verify address matches if (parsed.address.toLowerCase() !== params.address.toLowerCase()) { return { success: false, error: 'Address mismatch' } } // 4. Verify nonce matches if (parsed.nonce !== params.nonce) { return { success: false, error: 'Nonce mismatch' } } // 5. Check message isn't expired (5-minute window) const issuedAt = new Date(parsed.issuedAt).getTime() const now = Date.now() if (now - issuedAt > 5 * 60 * 1000) { return { success: false, error: 'Message expired' } } // 6. Verify the signature const isValid = await verifyMessage({ address: params.address as `0x${string}`, message: params.message, signature: params.signature as `0x${string}`, }) if (!isValid) { return { success: false, error: 'Invalid signature' } } return { success: true } }

Client Implementation

The client-side component (siwe.client.ts) provides utilities for the frontend.

Message Creation

The createSiweMessage function generates EIP-4361 compliant messages:

export function createSiweMessage(params: { address: string nonce: string statement?: string }): string { const domain = getDomain() const uri = getUri() const chainId = getTargetChainId() const issuedAt = new Date().toISOString() const statement = params.statement || 'Sign in to RitoSwap' return `${domain} wants you to sign in with your Ethereum account: ${params.address} ${statement} URI: ${uri} Version: 1 Chain ID: ${chainId} Nonce: ${params.nonce} Issued At: ${issuedAt}` }

The message format follows the EIP-4361 standard exactly, ensuring compatibility with all wallets.

API Reference

Server Functions

FunctionSignatureReturnsNotes
isSiweEnabled(): booleanbooleanChecks if SIWE is properly configured by validating Redis environment variables
getDomain(requestHeaders?: Headers): stringDomain stringResolves domain with priority: env override → request headers → Vercel URL → localhost
generateNonce(identifier: string): Promise<string>8-character nonceGenerates cryptographically random nonce, stores in Redis with 5-minute TTL
verifyNonce(identifier: string, nonce: string): Promise<boolean>booleanChecks nonce validity and consumes it (one-time use)
verifySiweMessage(params: {message, signature, nonce, address, requestHeaders?}): Promise<{success, error?}>{success: boolean, error?: string}Comprehensive EIP-4361 message verification including domain, address, nonce, timestamp, and signature
parseSiweMessage(message: string): ParsedMessage | nullParsed message object or nullInternal function that extracts fields from SIWE-formatted message
generateRandomNonce(): string8-character stringInternal function using Math.random() for nonce generation

Client Functions

FunctionSignatureReturnsNotes
isSiweEnabled(): booleanbooleanClient-side check for SIWE activation via env var
getDomain(): stringDomain stringClient-side domain resolution with same priority as server
getUri(): stringFull URI with protocolConstructs complete URI including http/https protocol
createSiweMessage(params: {address, nonce, statement?}): stringEIP-4361 formatted messageGenerates standard SIWE message for wallet signing
getTargetChainIdReferenced but importedChain ID numberImported from chainConfig utils

Types and Interfaces

interface SiweMessageParams { address: string // Ethereum address nonce: string // Generated nonce statement?: string // Optional custom statement } interface VerifyMessageParams { message: string // Full SIWE message signature: string // Wallet signature nonce: string // Nonce to verify address: string // Claimed address requestHeaders?: Headers // Optional headers for domain } interface VerifyResult { success: boolean error?: string // Specific error message if failed }

Integration in RitoSwap

Gate Access Flow

When a user clicks “Sign & Unlock” in the GateModal component:

Step 1: Check SIWE Status

if (isSiweEnabled()) { // Use SIWE flow } else { // Fall back to legacy timestamp-based signing }

Step 2: Get Nonce

const nonceResponse = await fetch('/api/nonce') if (!nonceResponse.ok) { throw new Error('Failed to get nonce') } const { nonce } = await nonceResponse.json()

Step 3: Create Message

message = createSiweMessage({ address, nonce, statement: `Sign in to access token gate with key #${tokenId}` })

Step 4: Request Signature

const signature = await signMessageAsync({ message }) // On mobile with WalletConnect, open the wallet app if (isMobileDevice() && connector?.id === 'walletConnect') { openWallet() }

Step 5: Verify Access

const response = await fetch('/api/gate-access', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address, signature, tokenId, message, nonce }) })

Content Submission Flow

When users submit messages within the gated area, a different signature pattern is used:

// In GatePageWrapper.tsx handleGatedSubmission const timestamp = Date.now() const signMessage = `Token Gate Access Request: Token ID: ${tokenId} Address: ${address} Timestamp: ${timestamp} Message: ${text}` const signature = await signMessageAsync({ message: signMessage }) // Submit to verify-token-gate endpoint const res = await fetch('/api/verify-token-gate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tokenId, message: text, signature, signMessage, address, timestamp }) })

This secondary flow doesn’t use SIWE format but maintains security through timestamp validation and signature verification.

API Endpoints Using SIWE

/api/nonce

Generates nonces for SIWE authentication:

export async function GET(req: NextRequest) { if (!isSiweEnabled()) { return NextResponse.json({ error: 'SIWE not enabled' }, { status: 501 }) } const identifier = getIdentifier(req) const nonce = await generateNonce(identifier) return NextResponse.json({ nonce }) }

/api/gate-access

Verifies SIWE signatures for gate access:

if (isSiweEnabled()) { // Verify SIWE message const verifyResult = await verifySiweMessage({ message, signature, nonce, address, requestHeaders: req.headers }) if (!verifyResult.success) { return NextResponse.json( { error: verifyResult.error }, { status: 401 } ) } // Verify nonce const identifier = getIdentifier(req) const nonceValid = await verifyNonce(identifier, nonce) if (!nonceValid) { return NextResponse.json( { error: 'Invalid or expired nonce' }, { status: 401 } ) } }

Testing

The library includes comprehensive tests in siwe.server.test.ts:

Configuration Tests

describe('isSiweEnabled', () => { it('returns true when all env vars are properly set', () => { process.env.NEXT_PUBLIC_ACTIVATE_REDIS = 'true' process.env.KV_REST_API_URL = 'https://test-api.upstash.io' process.env.KV_REST_API_TOKEN = 'test-key' expect(isSiweEnabled()).toBe(true) }) it('returns false when API URL is "false"', () => { process.env.KV_REST_API_URL = 'false' expect(isSiweEnabled()).toBe(false) }) })

Domain Resolution Tests

describe('getDomain', () => { it('strips protocol from domain env', () => { process.env.NEXT_PUBLIC_DOMAIN = 'https://app.ritoswap.com' expect(getDomain()).toBe('app.ritoswap.com') }) it('falls back to request headers', () => { const headers = new Headers({ host: 'test.ritoswap.com' }) expect(getDomain(headers)).toBe('test.ritoswap.com') }) })

Message Verification Tests

it('verifies valid SIWE message', async () => { vi.mocked(verifyMessage).mockResolvedValue(true) const result = await verifySiweMessage({ message: validSiweMessage, signature: '0xvalidsignature', nonce: 'abc12345', address: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8', requestHeaders: new Headers({ host: 'app.ritoswap.com' }) }) expect(result).toEqual({ success: true }) })

Environment Configuration

Required environment variables for SIWE functionality:

VariablePurposeExample
NEXT_PUBLIC_ACTIVATE_REDISEnable SIWE and rate limitingtrue
KV_REST_API_URLUpstash Redis REST API URLhttps://xxx.upstash.io
KV_REST_API_TOKENUpstash Redis API tokenAcXXXXX...
NEXT_PUBLIC_DOMAINOverride domain detectionhttps://ritoswap.com

Troubleshooting

Common Issues

⚠️

“SIWE not enabled” Error

This occurs when the required environment variables aren’t properly configured. Check:

  1. NEXT_PUBLIC_ACTIVATE_REDIS is set to "true" (string, not boolean)
  2. KV_REST_API_URL is a valid Upstash URL (not “false” or empty)
  3. KV_REST_API_TOKEN is present and valid

Domain Mismatch Errors

If users see “Domain mismatch” errors:

  1. Check the NEXT_PUBLIC_DOMAIN environment variable
  2. Ensure it matches the actual domain users are accessing
  3. Remove the protocol (https://) from the domain value
  4. For local development, leave it unset to auto-detect

Nonce Expiration

Nonces expire after 5 minutes. If users take too long to sign:

  1. The UI should show “Message expired” error
  2. Users need to restart the authentication flow
  3. Consider showing a countdown timer in the UI

Summary

The SIWE library provides RitoSwap’s secure authentication infrastructure for token-gated access. By leveraging Ethereum signatures and Redis-backed nonce management, it ensures only legitimate token holders can access restricted content while maintaining a smooth user experience. The library’s graceful degradation, comprehensive security checks, and mobile wallet support make it a robust solution for Web3 authentication in the RitoSwap ecosystem.