Skip to main content

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: useCallback prevents 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 siwe package: 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

Resources