Learn how to implement Sign-In with Ethereum (SIWE) authentication using React and Go. This guide covers frontend nonce signing, backend signature verification, and JWT token generation — the complete Web3 login flow for integrating Ethereum wallet login into your app.
Written by: Chia1104 CC BY-NC-SA 4.0
The frontend first requests a random number (nonce) from the backend.

The frontend then uses this nonce to get a signature and a message.

example.com wants you to sign in with your Ethereum account:
12345678
wallet_sign_statement
URI: https://example.com
Version: 1
Chain ID: 1
Nonce: 22ppxlq5
Issued At: 2025-02-19T03:57:48.796Z
Expiration Time: 2025-03-21T03:57:48.796Zimport * as React from 'react'
import { useAccount, useNetwork, useSignMessage } from 'wagmi'
import { SiweMessage } from 'siwe'
function SignInButton({
onSuccess,
onError,
}: {
onSuccess: (args: { address: string }) => void
onError: (args: { error: Error }) => void
}) {
const [state, setState] = React.useState<{
loading?: boolean
nonce?: string
}>({})
const fetchNonce = async () => {
try {
const nonceRes = await fetch('/api/nonce')
const nonce = await nonceRes.text()
setState((x) => ({ ...x, nonce }))
} catch (error) {
setState((x) => ({ ...x, error: error as Error }))
}
}
// Pre-fetch random nonce when button is rendered
// to ensure deep linking works for WalletConnect
// users on iOS when signing the SIWE message
React.useEffect(() => {
fetchNonce()
}, [])
const { address } = useAccount()
const { chain } = useNetwork()
const { signMessageAsync } = useSignMessage()
const signIn = async () => {
try {
const chainId = chain?.id
if (!address || !chainId) return
setState((x) => ({ ...x, loading: true }))
// Create SIWE message with pre-fetched nonce and sign with wallet
const message = new SiweMessage({
domain: window.location.host,
address,
statement: 'Sign in with Ethereum to the app.',
uri: window.location.origin,
version: '1',
chainId,
nonce: state.nonce,
})
const signature = await signMessageAsync({
message: message.prepareMessage(),
})
// Verify signature
const verifyRes = await fetch('/api/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message, signature }),
})
if (!verifyRes.ok) throw new Error('Error verifying message')
setState((x) => ({ ...x, loading: false }))
onSuccess({ address })
} catch (error) {
setState((x) => ({ ...x, loading: false, nonce: undefined }))
onError({ error: error as Error })
fetchNonce()
}
}
return (
<button disabled={!state.nonce || state.loading} onClick={signIn}>
Sign-In with Ethereum
</button>
)
}
export function Profile() {
const { isConnected } = useAccount()
const [state, setState] = React.useState<{
address?: string
error?: Error
loading?: boolean
}>({})
// Fetch user when:
React.useEffect(() => {
const handler = async () => {
try {
const res = await fetch('/api/me')
const json = await res.json()
setState((x) => ({ ...x, address: json.address }))
} catch (_error) {}
}
// 1. page loads
handler()
// 2. window is focused (in case user logs out of another window)
window.addEventListener('focus', handler)
return () => window.removeEventListener('focus', handler)
}, [])
if (isConnected) {
return (
<div>
{/* Account content goes here */}
{state.address ? (
<div>
<div>Signed in as {state.address}</div>
<button
onClick={async () => {
await fetch('/api/logout')
setState({})
}}
>
Sign Out
</button>
</div>
) : (
<SignInButton
onSuccess={({ address }) => setState((x) => ({ ...x, address }))}
onError={({ error }) => setState((x) => ({ ...x, error }))}
/>
)}
</div>
)
}
return <div>{/* Connect wallet content goes here */}</div>
}The backend needs to decode the message and signature, verify that the nonce matches, and return a token.

// This is a simple example of how to implement a web3 login endpoint using the siwe-go library.
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/golang-jwt/jwt/v5"
"github.com/spruceid/siwe-go"
)
const port = ":8080"
func main() {
http.HandleFunc("/api/v1/auth/web3:login", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
err := Web3Login(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
err := http.ListenAndServe(port, nil)
if err != nil {
fmt.Println("Error starting server:", err)
}
}
// verifyWalletSignature verifies the signature of a wallet address
func verifyWalletSignature(messageStr string, sig string) (jwt.MapClaims, error) {
message, err := siwe.ParseMessage(messageStr)
if err != nil {
err = fmt.Errorf("parse message err: %v", err)
return nil, err
}
verify, err := message.ValidNow()
if err != nil {
err = fmt.Errorf("verify message err: %v", err)
return nil, err
}
if !verify {
err = fmt.Errorf("verify message fail: %v", err)
return nil, err
}
publicKey, err := message.VerifyEIP191(sig)
if err != nil {
err = fmt.Errorf("verifyEIP191 err: %v", err)
return nil, err
}
pubBytes := crypto.FromECDSAPub(publicKey)
publicKeyString := hexutil.Encode(pubBytes)
// Return the verified claims
claims := jwt.MapClaims{
"web3_pub_key": publicKeyString,
}
return claims, nil
}
// LoginRequest represents the login request body
type LoginRequest struct {
Message string `json:"message"`
Signature string `json:"signature"`
}
func Web3Login(w http.ResponseWriter, r *http.Request) error {
// Parse the login request body
req := new(LoginRequest)
// 解析 JSON 請求體
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(req)
if err != nil {
return errors.New("invalid request body")
}
// Verify the signature
claims, err := verifyWalletSignature(req.Message, req.Signature)
if err != nil {
return errors.New(err.Error())
}
// Generate a JWT token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
return errors.New("error creating JWT token")
}
// Set the JWT token in the response header
http.SetCookie(w, &http.Cookie{
Name: "jwt_token",
Value: tokenString,
Expires: time.Now().Add(time.Hour * 24),
})
// Return a success response
w.WriteHeader(http.StatusOK)
return nil
}