Skip to Content
Welcome to RitoSwap's documentation!
DAppAPINonce Generation

Nonce Generation API

The Nonce Generation API provides cryptographically secure nonces for Sign-In with Ethereum (SIWE) authentication flows. This endpoint is the first step in establishing secure, wallet-based authentication sessions in the RitoSwap dApp.

Overview

In the world of Web3 authentication, nonces (numbers used once) play a critical role in preventing replay attacks and ensuring that authentication requests are fresh and legitimate. When a user signs in with their Ethereum wallet, they’re actually signing a message that includes this nonce, proving they control the private key associated with their address without ever exposing that key.

Understanding SIWE Authentication

Sign-In with Ethereum (SIWE) represents a paradigm shift in authentication. Instead of passwords stored on servers, users prove their identity through cryptographic signatures. The flow works like this:

  1. Client requests a nonce - A unique, unpredictable value tied to the session
  2. Wallet signs a message - The message includes the nonce, preventing replay attacks
  3. Server verifies signature - Confirms the signature matches the claimed address
  4. Session established - User is authenticated without passwords or personal data

The nonce is crucial because it ensures that even if someone intercepts a signed message, they cannot reuse it to impersonate the user later. Each authentication attempt requires a fresh nonce, making the signature valid only for that specific login attempt.

Why Nonces Matter

Consider what would happen without nonces: An attacker could capture your signed authentication message and replay it indefinitely to access your account. The nonce makes each signature unique and time-bound, similar to how a one-time password works in traditional two-factor authentication.

Endpoint Details

PropertyValue
URL/api/nonce
MethodGET
AuthenticationNone (public endpoint)
Response Typeapplication/json

Request Format

The nonce endpoint uses a simple GET request with no required parameters:

GET /api/nonce

The API automatically identifies the requester using their IP address or other identifying information from the request headers. This identifier is used for both rate limiting and nonce generation, ensuring each client receives unique nonces.

Request Headers and Client Identification

The API uses specific headers to identify clients for rate limiting:

EnvironmentHeader UsedSecurity Notes
Vercel Productionx-forwarded-forVercel overwrites client-supplied values, preventing spoofing
Local/Non-VercelSocket addressFalls back to request.ip or raw socket for security

Important: In production on Vercel, the x-forwarded-for header (and its aliases x-vercel-forwarded-for / x-real-ip) can be trusted because Vercel’s edge network overwrites any client-supplied values with the true source IP. In other environments, these headers can be spoofed, so the API falls back to the socket address.

Response Formats

Success Response

When SIWE is enabled and a nonce is successfully generated:

{ "nonce": "k8Jd93kdo0Sdk39dkD9dk3mdk93kd9Dk" }

HTTP Status Code: 200 OK

The nonce is a cryptographically secure random string that should be included in the SIWE message for signing.

Error Responses

The API provides specific error messages for different failure scenarios:

SIWE Not Enabled

{ "error": "SIWE not enabled" }

HTTP Status Code: 501 Not Implemented

This occurs when the SIWE feature is not configured on the server. The application needs proper environment configuration to enable SIWE functionality.

Rate Limit Exceeded

{ "error": "Too many requests", "limit": 10, "remaining": 0, "retryAfter": 45 }

HTTP Status Code: 429 Too Many Requests

Response Headers:

  • X-RateLimit-Limit: Maximum requests allowed (10)
  • X-RateLimit-Remaining: Requests remaining in window
  • Retry-After: Seconds until rate limit resets

Note: If the rate limit reset time is unavailable, the endpoint defaults the Retry-After header to 60 seconds.

Server Error

{ "error": "Failed to generate nonce" }

HTTP Status Code: 500 Internal Server Error

This indicates an internal error during nonce generation, possibly due to:

  • Redis connectivity issues
  • Cryptographic generation failures
  • Internal processing errors

Rate Limiting

The Nonce API implements careful rate limiting to prevent abuse while allowing legitimate authentication flows:

Rate Limit Configuration

  • Limit: 10 requests per 5 minutes per client identifier
  • Window: 5-minute sliding window
  • Identifier: IP address or similar unique client identifier
  • Namespace: Rate limits are scoped to ‘nonce’, so they won’t affect other API endpoints

The 10-request limit is calibrated to allow multiple authentication attempts while preventing nonce farming or denial-of-service attacks.

Intelligent Nonce Reuse

The API implements an optimization where nonces generated during rate limit checks can be reused:

// Check if we already have a nonce from the rate limit check let nonce = rateLimitResult.nonce // Generate new nonce if we don't have one if (!nonce) { nonce = await generateNonce(identifier) }

This design reduces redundant nonce generation and improves performance while maintaining security.

Nonce Expiration

Nonces are stored with a Time-To-Live (TTL) of 300 seconds (5 minutes). After this window, they automatically expire from Redis, and clients must fetch a fresh nonce if more than 5 minutes have passed since generation. This TTL balances security with user experience, giving users enough time to complete authentication while preventing long-lived nonces that could pose security risks.

SIWE Integration Flow

Understanding how the nonce API fits into the complete SIWE authentication flow helps in proper implementation:

Step 1: Request Nonce

The client requests a fresh nonce before initiating the sign-in process.

Step 2: Construct SIWE Message

Build a SIWE-compliant message including the nonce, domain, address, and other required fields.

Step 3: Request Signature

Prompt the user’s wallet to sign the message containing the nonce.

Step 4: Verify Authentication

Submit the signed message to the verification endpoint to establish the session.

Complete SIWE Example

Here’s a comprehensive example showing the full authentication flow with the gate access endpoint:

import { SiweMessage } from 'siwe'; class SiweAuthManager { private nonce: string | null = null; async authenticateForGateAccess(address: string, tokenId: number) { try { // Step 1: Get a fresh nonce const nonceResponse = await fetch('/api/nonce'); if (!nonceResponse.ok) { if (nonceResponse.status === 501) { throw new Error('SIWE authentication is not available'); } if (nonceResponse.status === 429) { const retryAfter = nonceResponse.headers.get('Retry-After'); throw new Error(`Rate limited. Try again in ${retryAfter} seconds`); } throw new Error('Failed to get nonce'); } const { nonce } = await nonceResponse.json(); this.nonce = nonce; // Step 2: Create SIWE message const message = new SiweMessage({ domain: window.location.host, address: address, statement: `Accessing gated content for token #${tokenId}`, uri: window.location.origin, version: '1', chainId: 1, // Or your target chain ID nonce: nonce, issuedAt: new Date().toISOString() }); const messageToSign = message.prepareMessage(); // Step 3: Request signature from wallet const signature = await this.requestSignature(messageToSign); // Step 4: Verify with gate access endpoint const verifyResponse = await fetch('/api/gate-access', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: address, signature: signature, tokenId: tokenId, message: messageToSign, nonce: nonce }) }); if (verifyResponse.ok) { const data = await verifyResponse.json(); if (data.success && data.access === 'granted') { console.log('Access granted!'); return data.content; // Returns the gated content } } else { const error = await verifyResponse.json(); throw new Error(error.error || 'Verification failed'); } } catch (error) { console.error('Authentication error:', error); throw error; } } private async requestSignature(message: string): Promise<string> { // Implementation depends on your wallet connection library // Example with ethers.js: const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner(); return await signer.signMessage(message); } }

React Integration

For React applications, create a custom hook to manage nonce fetching and gate access authentication:

import { useState, useCallback } from 'react'; import { SiweMessage } from 'siwe'; interface NonceState { nonce: string | null; loading: boolean; error: string | null; } export function useGateAccess() { const [state, setState] = useState<NonceState>({ nonce: null, loading: false, error: null }); const fetchNonce = useCallback(async () => { setState(prev => ({ ...prev, loading: true, error: null })); try { const response = await fetch('/api/nonce'); if (!response.ok) { const data = await response.json(); if (response.status === 429) { const retryAfter = response.headers.get('Retry-After'); throw new Error( `Rate limited. Please wait ${retryAfter} seconds before trying again.` ); } throw new Error(data.error || 'Failed to fetch nonce'); } const { nonce } = await response.json(); setState({ nonce, loading: false, error: null }); return nonce; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred'; setState({ nonce: null, loading: false, error: errorMessage }); throw error; } }, []); const authenticateForAccess = useCallback(async ( address: string, tokenId: number, signer: any // Your wallet signer instance ) => { try { // Get nonce if we don't have one const currentNonce = state.nonce || await fetchNonce(); // Create SIWE message const message = new SiweMessage({ domain: window.location.host, address: address, statement: `Accessing gated content for token #${tokenId}`, uri: window.location.origin, version: '1', chainId: 1, nonce: currentNonce, issuedAt: new Date().toISOString() }); const messageToSign = message.prepareMessage(); const signature = await signer.signMessage(messageToSign); // Submit to gate access endpoint const response = await fetch('/api/gate-access', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address, signature, tokenId, message: messageToSign, nonce: currentNonce }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Access denied'); } const data = await response.json(); // Clear nonce after use setState({ nonce: null, loading: false, error: null }); return data.content; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Authentication failed'; setState(prev => ({ ...prev, error: errorMessage })); throw error; } }, [state.nonce, fetchNonce]); return { ...state, fetchNonce, authenticateForAccess }; } // Usage in component function GatedContentAccess({ tokenId }: { tokenId: number }) { const { loading, error, authenticateForAccess } = useGateAccess(); const { address, signer } = useWallet(); // Your wallet hook const [content, setContent] = useState(null); const handleAccess = async () => { try { const gatedContent = await authenticateForAccess( address, tokenId, signer ); setContent(gatedContent); } catch (error) { console.error('Failed to access content:', error); } }; if (content) { return <div dangerouslySetInnerHTML={{ __html: content.welcomeText }} />; } return ( <button onClick={handleAccess} disabled={loading || !address} > {loading ? 'Authenticating...' : 'Access Gated Content'} </button> ); }

Security Considerations

The nonce generation system implements several security measures to ensure safe authentication:

Cryptographic Security

Nonces are generated using cryptographically secure random number generators, ensuring they cannot be predicted or manipulated. The generation process likely uses Node.js’s crypto.randomBytes() or similar secure methods.

Rate Limiting Protection

The 10-requests-per-5-minutes limit prevents attackers from:

  • Farming nonces for analysis
  • Overwhelming the server with requests
  • Conducting timing attacks

Session Binding

Nonces are typically bound to the requesting client’s identifier (IP address), preventing cross-client nonce usage. This adds an additional layer of security against replay attacks.

Temporal Validity

Nonces expire after 5 minutes (300 seconds) as enforced by Redis TTL. This balances security with user experience, preventing long-lived nonces that could be exploited while giving users reasonable time to complete authentication.

Configuration Requirements

To enable the nonce generation API, your application needs proper Redis configuration:

Environment Variables

# Enable Redis functionality NEXT_PUBLIC_ACTIVATE_REDIS=true # Redis connection via Upstash KV_REST_API_URL=https://your-redis-instance.upstash.io KV_REST_API_TOKEN=your-upstash-token

Checking SIWE Status

The API includes a built-in check for SIWE availability:

if (!isSiweEnabled()) { return NextResponse.json( { error: 'SIWE not enabled' }, { status: 501 } ) }

This ensures the API fails gracefully when SIWE is not properly configured, providing clear feedback to developers during setup.

Testing Strategies

Comprehensive testing ensures your nonce API works reliably. The nonce endpoint includes a complete unit test suite that covers all code paths:

Unit Testing

The nonce API is thoroughly tested using Vitest with mocked dependencies:

// app/api/nonce/__tests__/nonce.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest' import { NextRequest } from 'next/server' import { GET } from '../route' import * as siweServer from '@/app/lib/siwe/siwe.server' import * as rateLimitServer from '@/app/lib/rateLimit/rateLimit.server' vi.mock('@/app/lib/siwe/siwe.server') vi.mock('@/app/lib/rateLimit/rateLimit.server') describe('GET /api/nonce', () => { beforeEach(() => { vi.clearAllMocks() }) it('returns 501 when SIWE is disabled', async () => { vi.mocked(siweServer.isSiweEnabled).mockReturnValue(false) const req = new NextRequest('http://localhost:3000/api/nonce') const response = await GET(req) const data = await response.json() expect(response.status).toBe(501) expect(data.error).toBe('SIWE not enabled') }) it('returns 429 when rate limited', async () => { vi.mocked(siweServer.isSiweEnabled).mockReturnValue(true) vi.mocked(rateLimitServer.checkRateLimitWithNonce).mockResolvedValue({ success: false, limit: 10, remaining: 0, reset: Date.now() + 60000 }) const req = new NextRequest('http://localhost:3000/api/nonce') const response = await GET(req) const data = await response.json() expect(response.status).toBe(429) expect(data.error).toBe('Too many requests') expect(response.headers.get('Retry-After')).toBeTruthy() }) it('returns existing nonce from rate limit check', async () => { vi.mocked(siweServer.isSiweEnabled).mockReturnValue(true) vi.mocked(rateLimitServer.checkRateLimitWithNonce).mockResolvedValue({ success: true, nonce: 'existing-nonce' }) vi.mocked(rateLimitServer.getIdentifier).mockReturnValue('192.168.1.1') const req = new NextRequest('http://localhost:3000/api/nonce') const response = await GET(req) const data = await response.json() expect(response.status).toBe(200) expect(data.nonce).toBe('existing-nonce') expect(siweServer.generateNonce).not.toHaveBeenCalled() }) it('generates new nonce when none exists', async () => { vi.mocked(siweServer.isSiweEnabled).mockReturnValue(true) vi.mocked(rateLimitServer.checkRateLimitWithNonce).mockResolvedValue({ success: true }) vi.mocked(rateLimitServer.getIdentifier).mockReturnValue('192.168.1.1') vi.mocked(siweServer.generateNonce).mockResolvedValue('new-nonce') const req = new NextRequest('http://localhost:3000/api/nonce') const response = await GET(req) const data = await response.json() expect(response.status).toBe(200) expect(data.nonce).toBe('new-nonce') expect(siweServer.generateNonce).toHaveBeenCalledWith('192.168.1.1') }) it('returns 500 when nonce generation fails', async () => { vi.mocked(siweServer.isSiweEnabled).mockReturnValue(true) vi.mocked(rateLimitServer.checkRateLimitWithNonce).mockResolvedValue({ success: true }) vi.mocked(rateLimitServer.getIdentifier).mockReturnValue('192.168.1.1') vi.mocked(siweServer.generateNonce).mockRejectedValue(new Error('Redis connection failed')) const req = new NextRequest('http://localhost:3000/api/nonce') const response = await GET(req) const data = await response.json() expect(response.status).toBe(500) expect(data.error).toBe('Failed to generate nonce') expect(siweServer.generateNonce).toHaveBeenCalledWith('192.168.1.1') }) })

This test suite provides complete coverage of all possible paths through the nonce endpoint:

  1. SIWE Disabled (501): Tests that the endpoint returns appropriate error when SIWE functionality is not enabled
  2. Rate Limited (429): Verifies rate limiting behavior and proper headers are returned
  3. Existing Nonce Reuse: Confirms the optimization where nonces from rate limit checks are reused without regeneration
  4. New Nonce Generation: Tests successful nonce generation when no cached nonce exists
  5. Generation Failure (500): Ensures proper error handling when nonce generation fails due to Redis or other issues

The tests use Vitest’s mocking capabilities to isolate the route handler from its dependencies, ensuring fast and reliable test execution. Each test follows the Arrange-Act-Assert pattern for clarity and maintainability.

Troubleshooting Guide

Common issues and their solutions:

“SIWE not enabled” Error

Problem: The API returns a 501 error indicating SIWE is not configured.

Solution: Ensure all required environment variables are set:

  • NEXT_PUBLIC_ACTIVATE_REDIS=true
  • KV_REST_API_URL is configured with your Upstash Redis URL
  • KV_REST_API_TOKEN is set with your Upstash access token

Rate Limiting During Development

Problem: Hitting rate limits during testing slows down development.

Solution: Consider implementing a development mode bypass:

const isDevelopment = process.env.NODE_ENV === 'development'; const rateLimit = isDevelopment ? 100 : 10; // Higher limit in dev

Nonce Expiration Issues

Problem: Users report authentication failures due to expired nonces.

Solution: Implement clear user feedback and automatic retry:

if (error.message.includes('expired nonce')) { // Clear old nonce clearNonce(); // Show user-friendly message showMessage('Session expired. Please try signing in again.'); // Optionally, auto-retry with fresh nonce const freshNonce = await fetchNonce(); retryAuthentication(freshNonce); }

Summary

The Nonce Generation API forms the foundation of secure, decentralized authentication in RitoSwap. By providing cryptographically secure, rate-limited nonces with a 5-minute TTL, it enables users to prove their identity through wallet signatures rather than traditional passwords.

Understanding the role of nonces in preventing replay attacks and ensuring authentication freshness is crucial for implementing secure Web3 applications. The API’s careful implementation of rate limiting (10 requests per 5 minutes), client identification through secure headers on Vercel, and automatic nonce expiration ensures both security and usability.

Whether you’re building a simple sign-in flow or a complex authentication system, this API provides the secure, reliable nonce generation needed for SIWE integration, embodying the best practices of modern Web3 development.