Web3 Authentication Guide: Wallet Connect, Sign-In with Ethereum, and Session Management
Authentication in Web3 is fundamentally different from traditional web authentication. Instead of passwords and email verification, users prove ownership of a cryptographic key by signing messages with their wallet. This shift eliminates password-related security risks but introduces new challenges around session management, wallet compatibility, and user experience.
How Web3 Authentication Works
Web3 authentication follows the Sign-In with Ethereum (SIWE) standard (EIP-4361):
1. The user connects their wallet (MetaMask, WalletConnect, etc.)
2. The dApp generates a unique message containing a nonce, domain, and timestamp
3. The user signs the message with their private key
4. The signature is verified on the backend
5. A session is created (JWT or cookie-based)
Setting Up Web3 Auth
git clone https://github.com/breafio/web3-auth-kit.git my-dapp
cd my-dapp
pnpm install
cp .env.example .env.localConfiguration
# .env.local
NEXT_PUBLIC_WALLET_CONNECT_ID=your-walletconnect-project-id
NEXT_PUBLIC_ALCHEMY_API_KEY=your-alchemy-key
NEXT_PUBLIC_CHAIN_ID=1
NEXT_PUBLIC_DOMAIN=localhost:3000
SESSION_SECRET=your-secret-key-at-least-32-chars-longMessage Generation (EIP-4361)
The SIWE message must follow the standard format:
// lib/auth/siwe.ts
import { generateNonce } from 'siwe'
import { SiweMessage } from 'siwe'
export function createSiweMessage(
address: string,
domain: string,
nonce: string,
chainId: number
): string {
const message = new SiweMessage({
domain,
address,
statement: 'Sign in to BreafIO with your Ethereum wallet.',
uri: `https://${domain}`,
version: '1',
chainId,
nonce,
issuedAt: new Date().toISOString(),
})
return message.prepareMessage()
}Wallet Connection with RainbowKit
RainbowKit provides a beautiful, customizable wallet connection UI:
// app/providers.tsx
'use client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RainbowKitProvider, darkTheme, connectorsForWallets } from '@rainbow-me/rainbowkit'
import { metaMaskWallet, walletConnectWallet, coinbaseWallet, rainbowWallet } from '@rainbow-me/rainbowkit/wallets'
import { createConfig, http } from 'wagmi'
import { mainnet, polygon, arbitrum, optimism, base } from 'wagmi/chains'
import '@rainbow-me/rainbowkit/styles.css'
const projectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_ID!
const connectors = connectorsForWallets(
[
{
groupName: 'Popular',
wallets: [metaMaskWallet, rainbowWallet, coinbaseWallet, walletConnectWallet],
},
],
{ projectId }
)
const config = createConfig({
connectors,
chains: [mainnet, polygon, arbitrum, optimism, base],
transports: {
[mainnet.id]: http(),
[polygon.id]: http(),
[arbitrum.id]: http(),
[optimism.id]: http(),
[base.id]: http(),
},
})
const queryClient = new QueryClient()
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider theme={darkTheme()} coolMode>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
)
}Backend Verification
The authentication API verifies signatures and creates sessions:
// app/api/auth/verify/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { SiweMessage } from 'siwe'
import { createSession } from '@/lib/auth/session'
export async function POST(request: NextRequest) {
try {
const { message, signature } = await request.json()
const siweMessage = new SiweMessage(message)
const fields = await siweMessage.verify({ signature })
if (fields.error) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
)
}
// Create a session for the authenticated user
const session = await createSession({
address: fields.data.address,
chainId: fields.data.chainId,
nonce: fields.data.nonce,
})
return NextResponse.json({ success: true, session })
} catch (error) {
return NextResponse.json(
{ error: 'Authentication failed' },
{ status: 500 }
)
}
}Session Management
Sessions can be managed with JWTs or server-side cookies:
// lib/auth/session.ts
import { cookies } from 'next/headers'
import { jwtVerify, SignJWT } from 'jose'
const secret = new TextEncoder().encode(process.env.SESSION_SECRET!)
interface SessionData {
address: string
chainId: number
nonce: string
}
export async function createSession(data: SessionData): Promise<string> {
const token = await new SignJWT({ ...data })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(secret)
cookies().set('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60, // 7 days
})
return token
}
export async function getSession(): Promise<SessionData | null> {
const cookieStore = cookies()
const token = cookieStore.get('session')?.value
if (!token) return null
try {
const { payload } = await jwtVerify(token, secret)
return payload as unknown as SessionData
} catch {
return null
}
}Authentication Hook
A React hook that wraps the authentication flow:
// lib/auth/useAuth.ts
'use client'
import { useState, useCallback } from 'react'
import { useAccount, useSignMessage, useDisconnect } from 'wagmi'
export function useAuth() {
const { address, isConnected } = useAccount()
const { signMessageAsync } = useSignMessage()
const { disconnect } = useDisconnect()
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const login = useCallback(async () => {
if (!address) return
setIsLoading(true)
try {
// Get nonce from server
const nonceRes = await fetch('/api/auth/nonce')
const { nonce } = await nonceRes.json()
// Create SIWE message
const messageRes = await fetch('/api/auth/message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, nonce }),
})
const { message } = await messageRes.json()
// Sign the message
const signature = await signMessageAsync({ message })
// Verify on server
const verifyRes = await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature }),
})
if (verifyRes.ok) {
setIsAuthenticated(true)
}
} catch (error) {
console.error('Authentication failed:', error)
} finally {
setIsLoading(false)
}
}, [address, signMessageAsync])
const logout = useCallback(() => {
fetch('/api/auth/logout', { method: 'POST' })
disconnect()
setIsAuthenticated(false)
}, [disconnect])
return {
address,
isConnected,
isAuthenticated,
isLoading,
login,
logout,
}
}The Bottom Line
Web3 authentication eliminates password risks but requires careful implementation of the SIWE standard, session management, and wallet compatibility. The Web3 Auth Kit provides a complete, production-ready authentication system that works with all major wallets and includes backend verification, session management, and a clean React hook API.
Related Template
Try this production-ready starter kit to build your project faster.
Connected Wallets
MetaMask
12.45 ETH
WalletConnect
2,450 USDC
Coinbase Wallet
Not connected
Supported Chains
Ethereum
Polygon
Arbitrum
Optimism
Base
Avalanche
Web3 Auth Kit
Wallet authentication for any dApp
Ready to Build?
Get started with our production-ready starter kits and ship your project faster.
Browse Starter Kits