Secure Nonces for Sign In with Ethereum
This recipe demonstrates how to generate secure nonces for Sign In with Ethereum (SIWE) using SpaceComputer Orbitport's cTRNG service. Unlike traditional pseudo-random number generators, this implementation uses genuine cosmic randomness from satellite instrumentation to create truly unpredictable nonces that prevent replay attacks.
Prerequisites
- TypeScript/JavaScript knowledge
- Next.js or React application
- Wagmi or ethers.js for wallet integration
- API access to SpaceComputer Orbitport (Get your API access key here)
- Understanding of SIWE (EIP-4361)
What You'll Build
A secure SIWE authentication system that:
- Generates cosmic randomness-based nonces to prevent replay attacks
- Implements server-side nonce storage with iron-session
- Provides secure signature verification
- Includes fallback mechanisms for offline resilience
- Demonstrates the complete SIWE authentication flow
🏗️ Architecture Pattern Rationale
This recipe implements a Cosmic Nonce Pattern for SIWE authentication:
- Cosmic Entropy: Server-side retrieval of truly random nonces from space
- Secure Storage: HTTP-only cookies prevent client-side nonce tampering
- Fallback Resilience: Standard CSPRNG nonces when cosmic randomness is unavailable
- Verifiable Randomness: Nonces can be verified for cosmic origin
- Anti-Replay: Session-based nonce management prevents signature reuse
Implementation Steps
1. Environment Configuration
Set up your environment variables:
# .env.local
ORBITPORT_CLIENT_ID=your_client_id
ORBITPORT_CLIENT_SECRET=your_client_secret
ORBITPORT_AUTH_URL=https://your-auth-domain.auth0.com
ORBITPORT_API_URL=https://op.spacecomputer.io
2. Authentication Setup
🎯 Design Rationale for Production
Server-side authentication is critical for SIWE nonce generation because:
- Security: API credentials are never exposed to client-side code, preventing credential theft
- Token Management: Centralized token lifecycle management reduces security vulnerabilities
- Rate Limiting: Server-side control allows for proper API rate limiting and abuse prevention
- Audit Trail: All nonce generation requests can be logged and monitored
- Scalability: Multiple server instances can share the same authentication logic
Create a centralized authentication utility in lib/auth.ts:
// lib/auth.ts
import { NextApiRequest, NextApiResponse } from "next";
import { parseToken, decrypt, encrypt } from "./crypto"; // You'll need to implement these
const TOKEN_EXPIRE_BUFFER = 300; // 5 minutes buffer
async function generateAccessToken(): Promise<string | null> {
const clientId = process.env.ORBITPORT_CLIENT_ID;
const clientSecret = process.env.ORBITPORT_CLIENT_SECRET;
const authUrl = process.env.ORBITPORT_AUTH_URL;
if (!clientId || !clientSecret || !authUrl) {
throw new Error("Missing Orbitport authentication configuration");
}
try {
const response = await fetch(`${authUrl}/oauth/token`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
audience: "https://op.spacecomputer.io/api",
grant_type: "client_credentials",
}),
});
if (!response.ok) {
throw new Error("Failed to get access token");
}
const data = await response.json();
return data.access_token;
} catch (error) {
console.error("Error getting access token:", error);
return null;
}
}
export async function getValidToken(
req: NextApiRequest,
res: NextApiResponse
): Promise<string | null> {
try {
const encryptedToken = getEncryptedTokenFromCookies(req);
let accessToken: string | null = null;
if (encryptedToken) {
const decrypted = decrypt(encryptedToken);
const parsed = parseToken(decrypted);
if (parsed) {
const now = Math.floor(Date.now() / 1000);
// Only use token if not expired and not about to expire
if (parsed.exp > now + TOKEN_EXPIRE_BUFFER) {
accessToken = parsed.access_token;
}
}
}
// If no valid token, generate a new one
if (!accessToken) {
accessToken = await generateAccessToken();
if (!accessToken) {
console.error("Failed to get new access token");
return null;
}
const parsed = parseToken(accessToken);
if (!parsed) {
console.error("Failed to parse new token");
return null;
}
setEncryptedTokenCookie(res, accessToken, parsed.exp);
}
return accessToken;
} catch (error) {
console.error("Error in getValidToken:", error);
return null;
}
}
🔐 Token Expiry Management Rationale
The TOKEN_EXPIRE_BUFFER (5-minute buffer) is essential for production because:
- Proactive Refresh: Tokens are refreshed before they expire, preventing user-facing errors
- Graceful Degradation: If token refresh fails, the buffer provides time for fallback mechanisms
- Load Distribution: Staggered token refreshes prevent all users from hitting the auth endpoint simultaneously
- Security: Reduces the window where expired tokens might be used
- User Experience: Eliminates authentication timeouts during active user sessions
3. Session Management Setup
Install iron-session for secure session management:
npm install iron-session
Create session configuration in lib/session.ts:
// lib/session.ts
import { IronSessionData, getIronSession } from "iron-session";
import { NextRequest } from "next/server";
import { SessionOptions } from "iron-session";
const sessionOptions: SessionOptions = {
password:
process.env.SESSION_SECRET ||
"your-super-secret-password-at-least-32-characters",
cookieName: "cosmic-siwe-session",
cookieOptions: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
},
};
export interface SessionData extends IronSessionData {
nonce?: string;
}
export async function getSession(req: NextRequest) {
return getIronSession<SessionData>(req, res, sessionOptions);
}
🍪 Session Management Rationale
Using iron-session with HTTP-only cookies provides:
- Security: Nonces stored server-side prevent client-side tampering
- Cookie Security: HTTP-only cookies cannot be accessed via JavaScript
- Session Isolation: Each user has their own secure session
- Nonce Verification: Nonces can be verified against session storage
- Anti-Replay: Destroying session after verification prevents nonce reuse
4. API Route for Nonce Generation
Create an API endpoint in app/api/nonce/route.ts with fallback support:
// app/api/nonce/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/session";
import { generateNonce } from "siwe";
import { getValidToken } from "@/lib/auth";
const ORBITPORT_API_URL = process.env.ORBITPORT_API_URL;
export async function GET(req: NextRequest) {
let nonce: string;
let usedFallback = false;
try {
// Try to get cosmic randomness first
if (ORBITPORT_API_URL) {
const accessToken = await getValidToken(req, {} as any);
if (accessToken) {
const response = await fetch(
`${ORBITPORT_API_URL}/api/v1/services/trng`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
if (response.ok) {
const data = await response.json();
nonce = data.data; // Use cosmic randomness as nonce
console.log("Generated cosmic nonce:", nonce);
} else {
throw new Error(`Orbitport API failed: ${response.status}`);
}
} else {
throw new Error("Failed to get access token");
}
} else {
throw new Error("Missing Orbitport API URL");
}
} catch (error) {
console.warn("Using fallback nonce generation:", error);
usedFallback = true;
nonce = generateNonce(); // Fallback to standard nonce
console.log("Generated fallback nonce:", nonce);
}
const session = await getSession(req);
session.nonce = nonce;
await session.save();
return NextResponse.json({
nonce: session.nonce,
usedFallback,
});
}
🌐 Nonce Generation Rationale
Cosmic nonce generation with fallback provides:
- True Randomness: Uses cosmic radiation for unbiased nonce generation
- Security: Access tokens never leave your server, preventing token exposure
- Fallback Resilience: Standard CSPRNG ensures nonce generation works offline
- Anti-Replay: Server-side nonce storage prevents signature reuse
- Monitoring: Track usage patterns and implement alerting for production issues
- Compliance: Meet enterprise security requirements for authentication systems
5. SIWE Message Verification
Create a verification endpoint in app/api/verify/route.ts:
// app/api/verify/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/session";
import { SiweMessage } from "siwe";
export async function POST(req: NextRequest) {
try {
const { message, signature } = await req.json();
if (!message || !signature) {
return NextResponse.json(
{ ok: false, message: "Missing message or signature" },
{ status: 400 }
);
}
const session = await getSession(req);
if (!session.nonce) {
return NextResponse.json(
{ ok: false, message: "No nonce found in session" },
{ status: 400 }
);
}
const siweMessage = new SiweMessage(message);
const { data: fields } = await siweMessage.verify({
signature,
nonce: session.nonce,
});
if (fields.nonce !== session.nonce) {
return NextResponse.json(
{ ok: false, message: "Invalid nonce." },
{ status: 422 }
);
}
session.destroy();
return NextResponse.json({ ok: true });
} catch (error) {
console.error("Verification error:", error);
// Try to get session for cleanup
try {
const session = await getSession(req);
session.destroy();
} catch (sessionError) {
console.error("Error destroying session:", sessionError);
}
if (
(error as any).error?.type?.includes(
"Nonce does not match provided nonce for verification."
)
) {
return NextResponse.json(
{ ok: false, message: "Invalid nonce." },
{ status: 422 }
);
}
return NextResponse.json(
{ ok: false, message: (error as Error).message || "Verification failed" },
{ status: 500 }
);
}
}
🔒 Signature Verification Rationale
Server-side signature verification provides:
- Security: Nonce validation prevents replay attacks
- Session Management: Nonce is destroyed after successful verification
- Error Handling: Graceful error handling with proper HTTP status codes
- Anti-Replay: Session destruction prevents nonce reuse
- Compliance: Meets SIWE specification requirements for nonce handling
- Monitoring: Track verification success and failure rates
6. Client-Side SIWE Hook
Create a custom React hook in hooks/useSIWE.ts:
// hooks/useSIWE.ts
import { useCallback, useState } from "react";
import { useAccount, useSignMessage } from "wagmi";
import { SiweMessage } from "siwe";
interface SIWEState {
nonce: string | null;
message: SiweMessage | null;
signature: string | null;
verificationStatus: string;
isLoading: boolean;
usedFallback: boolean;
}
export function useSIWE() {
const { address, chainId, isConnected } = useAccount();
const { signMessageAsync } = useSignMessage();
const [state, setState] = useState<SIWEState>({
nonce: null,
message: null,
signature: null,
verificationStatus: "",
isLoading: false,
usedFallback: false,
});
const fetchNonce = useCallback(async () => {
try {
const response = await fetch("/api/nonce");
if (!response.ok) {
throw new Error("Failed to fetch nonce");
}
const data = await response.json();
setState((prev) => ({
...prev,
nonce: data.nonce,
usedFallback: data.usedFallback || false,
}));
return data;
} catch (error) {
console.error("Error fetching nonce:", error);
throw error;
}
}, []);
const signMessageWithCustomNonce = useCallback(
async (customNonce: string) => {
if (!isConnected || !address || !chainId) {
throw new Error("Missing required data for signing");
}
try {
setState((prev) => ({ ...prev, isLoading: true }));
const message = new SiweMessage({
domain: window.location.host,
address,
statement:
"Sign in with Ethereum using cosmic randomness for enhanced security.",
uri: window.location.origin,
version: "1",
chainId,
nonce: customNonce,
});
const preparedMessage = message.prepareMessage();
const signature = await signMessageAsync({ message: preparedMessage });
setState((prev) => ({
...prev,
message,
signature,
isLoading: false,
}));
return { message: preparedMessage, signature };
} catch (error) {
setState((prev) => ({ ...prev, isLoading: false }));
console.error("Error signing message:", error);
throw error;
}
},
[isConnected, address, chainId, signMessageAsync]
);
const signMessage = useCallback(async () => {
if (!state.nonce) {
throw new Error("Missing required data for signing");
}
return signMessageWithCustomNonce(state.nonce);
}, [state.nonce, signMessageWithCustomNonce]);
const verifySignature = useCallback(async () => {
if (!state.message || !state.signature) {
throw new Error("No message or signature to verify");
}
try {
setState((prev) => ({
...prev,
isLoading: true,
verificationStatus: "Verifying...",
}));
const response = await fetch("/api/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: state.message.prepareMessage(),
signature: state.signature,
}),
});
const data = await response.json();
if (response.ok && data.ok) {
setState((prev) => ({
...prev,
verificationStatus: "Success!",
isLoading: false,
}));
return true;
} else {
const errorMessage = data.message || "Verification failed";
setState((prev) => ({
...prev,
verificationStatus: `Failed: ${errorMessage}`,
isLoading: false,
}));
return false;
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Verification failed";
setState((prev) => ({
...prev,
verificationStatus: `Failed: ${errorMessage}`,
isLoading: false,
}));
throw error;
}
}, [state.message, state.signature]);
const reset = useCallback(() => {
setState({
nonce: null,
message: null,
signature: null,
verificationStatus: "",
isLoading: false,
usedFallback: false,
});
}, []);
return {
...state,
fetchNonce,
signMessage,
signMessageWithCustomNonce,
verifySignature,
reset,
isConnected,
address,
chainId,
};
}
⚛️ SIWE Hook Design Rationale
Custom hooks provide production benefits through:
- Reusability: The same hook can be used across multiple components
- State Management: Centralized loading and error states for better UX
- Testing: Easier to unit test business logic separately from UI components
- Performance:
useCallbackprevents unnecessary re-renders and API calls - Error Boundaries: Consistent error handling across the application
- Type Safety: TypeScript interfaces ensure data consistency
7. Usage Example
Here's how to use the SIWE hook in your application:
import { useSIWE } from "@/hooks/useSIWE";
function SIWEAuthentication() {
const {
nonce,
signature,
verificationStatus,
isLoading,
usedFallback,
fetchNonce,
signMessage,
verifySignature,
reset,
isConnected,
address,
chainId,
} = useSIWE();
// Step 1: Fetch nonce
const handleFetching = async () => {
if (!isConnected || !address || !chainId) return;
await fetchNonce();
};
// Step 2: Sign message once nonce is fetched
useEffect(() => {
const handleSigning = async () => {
await signMessage();
};
if (nonce) {
handleSigning();
}
}, [nonce]);
// Step 3: Verify signature
const handleVerify = async () => {
if (isLoading) return;
await verifySignature();
};
return (
<div>
<button onClick={handleFetching} disabled={!isConnected}>
Authenticate with Ethereum
</button>
{nonce && <p>Nonce: {nonce}</p>}
{signature && <p>Signature: {signature}</p>}
{verificationStatus && <p>Status: {verificationStatus}</p>}
{usedFallback && <p>Note: Used fallback randomness</p>}
</div>
);
}
🎲 SIWE Authentication Rationale
The SIWE implementation demonstrates production-ready patterns:
- Cosmic Nonces: Uses genuine randomness from space, not predictable algorithms
- Anti-Replay: Session-based nonce management prevents signature reuse
- Error Handling: Try-catch blocks prevent application crashes from API failures
- User Feedback: Clear status updates throughout the authentication flow
- Fallback Transparency: Clear indication when fallback randomness is used
- Security: Server-side verification prevents replay attacks
Key Benefits
- True Randomness: Leverages cosmic radiation for unbiased nonce generation
- Security: Server-side nonce management with session-based verification
- Reliability: Fallback mechanisms ensure authentication always works
- Anti-Replay: Session destruction after verification prevents signature reuse
- Flexibility: Works with any EIP-4361 compatible wallet
- Privacy: Nonces are stored server-side in secure HTTP-only cookies
Use Cases
- SIWE Authentication: Secure wallet-based authentication for dApps
- Signature Verification: Verify on-chain signatures with cosmic nonces
- Smart Contract Interactions: Generate secure nonces for contract calls
- Multi-sig Operations: Secure nonce generation for multi-signature wallets
- Enterprise Web3: Meet security requirements for blockchain applications
Production Considerations
🚀 Enterprise-Grade Authentication Security
This recipe follows production-ready patterns essential for secure SIWE authentication:
Security First Approach
- Cosmic Entropy: Uses genuine randomness from space, not predictable algorithms
- Credential Isolation: API keys are never exposed to client-side code
- Session Security: HTTP-only cookies prevent client-side nonce tampering
- Anti-Replay: Session destruction after verification prevents nonce reuse
- Signature Verification: Server-side verification prevents replay attacks
Reliability & Performance
- Fallback Resilience: Standard CSPRNG ensures authentication works offline
- Proactive Token Refresh: Eliminates authentication timeouts during user sessions
- Error Handling: Graceful degradation when external services are unavailable
- Performance: Efficient nonce generation with minimal computational overhead
Scalability & Monitoring
- Centralized Logic: Single source of truth for authentication across all server instances
- Performance Metrics: Track authentication success and failure rates
- Alerting: Proactive notifications for authentication failures or rate limit issues
- Horizontal Scaling: Authentication logic works seamlessly across multiple servers
Compliance & Governance
- Data Privacy: No sensitive data leaves your infrastructure
- Audit Trails: Complete logging for compliance and security investigations
- Rate Limiting: Prevent abuse and ensure fair usage across all users
- Verifiable Randomness: Nonces can be verified for cosmic origin
Next Steps
- Install the
siwepackage:npm i siwe - Install Wagmi for wallet integration:
npm i wagmi viem - Implement the crypto utility functions for token encryption/decryption
- Add nonce expiration logic for enhanced security
- Consider implementing rate limiting for the API endpoints
- Explore other Orbitport services like spaceTEE for secure computation