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:
- Client requests a nonce - A unique, unpredictable value tied to the session
- Wallet signs a message - The message includes the nonce, preventing replay attacks
- Server verifies signature - Confirms the signature matches the claimed address
- 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
Property | Value |
---|---|
URL | /api/nonce |
Method | GET |
Authentication | None (public endpoint) |
Response Type | application/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:
Environment | Header Used | Security Notes |
---|---|---|
Vercel Production | x-forwarded-for | Vercel overwrites client-supplied values, preventing spoofing |
Local/Non-Vercel | Socket address | Falls 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 windowRetry-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:
- SIWE Disabled (501): Tests that the endpoint returns appropriate error when SIWE functionality is not enabled
- Rate Limited (429): Verifies rate limiting behavior and proper headers are returned
- Existing Nonce Reuse: Confirms the optimization where nonces from rate limit checks are reused without regeneration
- New Nonce Generation: Tests successful nonce generation when no cached nonce exists
- 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 URLKV_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.