Add Sign-In with Ethereum (SIWE) authentication to your Scaffold-ETH 2 project with a single command.
npx create-eth@latest -e signinwithethereum/scaffold-siwe-extThis will create a new Scaffold-ETH 2 project with SIWE authentication pre-configured.
packages/nextjs/
├── app/
│ ├── api/siwe/
│ │ ├── nonce/route.ts # Generate cryptographic nonce
│ │ ├── verify/route.ts # Verify signature & create session
│ │ └── session/route.ts # Check session / logout
│ └── siwe/
│ └── page.tsx # Demo page with examples
├── hooks/
│ └── useSiwe.ts # React hook for SIWE auth
├── utils/
│ ├── siwe.ts # Session config & helpers
│ └── siwe.config.ts # Customizable settings
└── components/
└── Header.tsx.args.mjs # Adds SIWE link to nav
| Endpoint | Method | Description |
|---|---|---|
/api/siwe/nonce |
GET | Generate a cryptographically secure nonce |
/api/siwe/verify |
POST | Verify SIWE message and create session |
/api/siwe/session |
GET | Check current session status |
/api/siwe/session |
DELETE | Sign out (destroy session) |
- ✅ Domain validation - Prevents cross-site attacks
- ✅ Nonce validation - Prevents replay attacks
- ✅ Message expiration - Configurable time limit
- ✅ ERC-6492 support - Smart Contract Accounts work automatically
- ✅ Multi-chain - Ethereum, Polygon, Optimism, Arbitrum, Base, and more
import { useSiwe } from "~~/hooks/useSiwe";
function MyComponent() {
const { isSignedIn, address, signIn, signOut, isLoading, error } = useSiwe();
if (!isSignedIn) {
return (
<button onClick={signIn} disabled={isLoading}>
{isLoading ? "Signing in..." : "Sign in with Ethereum"}
</button>
);
}
return (
<div>
<p>Signed in as {address}</p>
<button onClick={signOut}>Sign Out</button>
</div>
);
}import { useSiwe } from "~~/hooks/useSiwe";
function ProtectedPage() {
const { isSignedIn, isLoading } = useSiwe();
if (isLoading) {
return <div>Loading...</div>;
}
if (!isSignedIn) {
return (
<div>
<h1>Access Denied</h1>
<p>Please sign in to view this content.</p>
</div>
);
}
return (
<div>
<h1>Secret Dashboard</h1>
<p>This content is only visible to authenticated users.</p>
</div>
);
}import { useSiwe } from "~~/hooks/useSiwe";
function SessionInfo() {
const { isSignedIn, address, chainId, signedInAt } = useSiwe();
if (!isSignedIn) return null;
return (
<div>
<p>Address: {address}</p>
<p>Chain ID: {chainId}</p>
<p>Signed in: {new Date(signedInAt!).toLocaleString()}</p>
</div>
);
}import { useSiwe } from "~~/hooks/useSiwe";
function SignInWithErrors() {
const { isSignedIn, signIn, isLoading, error } = useSiwe();
return (
<div>
{error && (
<div className="alert alert-error">
{error}
</div>
)}
{!isSignedIn && (
<button onClick={signIn} disabled={isLoading}>
Sign in with Ethereum
</button>
)}
</div>
);
}// app/api/protected/route.ts
import { cookies } from "next/headers";
import { getIronSession } from "iron-session";
import { SiweSessionData, sessionOptions } from "~~/utils/siwe";
export async function GET() {
const session = await getIronSession<SiweSessionData>(
await cookies(),
sessionOptions
);
if (!session.isLoggedIn) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
// User is authenticated - session.address is available
return Response.json({
message: `Hello ${session.address}!`,
data: "secret stuff"
});
}| Property | Type | Description |
|---|---|---|
isSignedIn |
boolean |
Whether the user is authenticated |
address |
Address | null |
The authenticated Ethereum address |
chainId |
number | null |
The chain ID from authentication |
signedInAt |
number | null |
Unix timestamp when session was created |
isLoading |
boolean |
Whether an operation is in progress |
error |
string | null |
Error message from last operation |
siweMessage |
string | null |
The SIWE message (for display) |
signIn |
() => Promise |
Initiate sign-in flow |
signOut |
() => Promise |
Sign out and destroy session |
checkSession |
() => Promise |
Manually check session status |
isWalletConnected |
boolean |
Whether a wallet is connected |
connectedAddress |
Address | undefined |
Currently connected wallet address |
Edit utils/siwe.config.ts to customize:
const siweConfig = {
// Session cookie duration (days)
sessionDurationDays: 7,
// How long user has to sign the message (minutes)
messageExpirationMinutes: 10,
// Statement shown in the SIWE message
statement: "Sign in with Ethereum to the app.",
};For production, you MUST set:
IRON_SESSION_SECRET=your-secret-at-least-32-characters-longGenerate a secure secret:
openssl rand -base64 32In development, a fallback secret is used automatically.
┌─────────────┐ 1. Click "Sign In" ┌─────────────┐
│ Browser │ ──────────────────────────► │ Server │
│ │ ◄────────────────────────── │ │
│ │ 2. Return nonce │ │
│ │ │ │
│ │ 3. Create SIWE message │ │
│ │ 4. Sign with wallet │ │
│ │ │ │
│ │ 5. Send message + sig │ │
│ │ ──────────────────────────► │ │
│ │ │ Verify │
│ │ 6. Set session cookie │ Signature │
│ │ ◄────────────────────────── │ │
│ │ │ │
│ ✓ Signed │ 7. Authenticated! │ ✓ Session │
│ In │ │ Created │
└─────────────┘ └─────────────┘
- viem - Native SIWE utilities (already in SE-2)
- wagmi - Wallet connection & message signing (already in SE-2)
- RainbowKit - Wallet UI components (already in SE-2)
- iron-session - Encrypted session cookies (only new dependency)
Sign-In with Ethereum (SIWE, EIP-4361) is an authentication standard that allows users to sign in to web applications using their Ethereum wallet. Instead of usernames and passwords, users prove ownership of their Ethereum address by signing a message.
Learn more at siwe.xyz.
Built for Scaffold-ETH 2.
MIT
