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:
- Gate Access Flow - When users attempt to unlock the token gate
- 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
Function | Signature | Returns | Notes |
---|---|---|---|
isSiweEnabled | (): boolean | boolean | Checks if SIWE is properly configured by validating Redis environment variables |
getDomain | (requestHeaders?: Headers): string | Domain string | Resolves domain with priority: env override → request headers → Vercel URL → localhost |
generateNonce | (identifier: string): Promise<string> | 8-character nonce | Generates cryptographically random nonce, stores in Redis with 5-minute TTL |
verifyNonce | (identifier: string, nonce: string): Promise<boolean> | boolean | Checks 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 | null | Parsed message object or null | Internal function that extracts fields from SIWE-formatted message |
generateRandomNonce | (): string | 8-character string | Internal function using Math.random() for nonce generation |
Client Functions
Function | Signature | Returns | Notes |
---|---|---|---|
isSiweEnabled | (): boolean | boolean | Client-side check for SIWE activation via env var |
getDomain | (): string | Domain string | Client-side domain resolution with same priority as server |
getUri | (): string | Full URI with protocol | Constructs complete URI including http/https protocol |
createSiweMessage | (params: {address, nonce, statement?}): string | EIP-4361 formatted message | Generates standard SIWE message for wallet signing |
getTargetChainId | Referenced but imported | Chain ID number | Imported 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:
Variable | Purpose | Example |
---|---|---|
NEXT_PUBLIC_ACTIVATE_REDIS | Enable SIWE and rate limiting | true |
KV_REST_API_URL | Upstash Redis REST API URL | https://xxx.upstash.io |
KV_REST_API_TOKEN | Upstash Redis API token | AcXXXXX... |
NEXT_PUBLIC_DOMAIN | Override domain detection | https://ritoswap.com |
Troubleshooting
Common Issues
“SIWE not enabled” Error
This occurs when the required environment variables aren’t properly configured. Check:
NEXT_PUBLIC_ACTIVATE_REDIS
is set to"true"
(string, not boolean)KV_REST_API_URL
is a valid Upstash URL (not “false” or empty)KV_REST_API_TOKEN
is present and valid
Domain Mismatch Errors
If users see “Domain mismatch” errors:
- Check the
NEXT_PUBLIC_DOMAIN
environment variable - Ensure it matches the actual domain users are accessing
- Remove the protocol (
https://
) from the domain value - For local development, leave it unset to auto-detect
Nonce Expiration
Nonces expire after 5 minutes. If users take too long to sign:
- The UI should show “Message expired” error
- Users need to restart the authentication flow
- 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.