Simple multi-chain SIWX authentication for Go
A zero-config, chain-agnostic Go package for SIWX (Sign-In With X) authentication supporting Ethereum (EIP-4361) and Solana (SIWS). One import, three functions: New(), Challenge(), Verify(). Works with all Go web frameworks.
- Zero-config defaults - Works out of the box
- Multi-chain support - Ethereum (EIP-4361) and Solana (SIWS)
- Framework agnostic - Works with any Go web framework
- JWT & Opaque tokens - Choose your token format
- Memory & Redis storage - Flexible nonce storage
- Security first - Replay protection, nonce validation, signature verification
go get github.com/heywinit/go-siwxpackage main
import "github.com/heywinit/go-siwx"
func main() {
client, _ := siwx.New()
challenge, _ := client.Challenge("0x...", "eip155:1")
session, token, _ := client.Verify(sig, "0x...", "eip155:1")
}package main
import (
"fmt"
"github.com/heywinit/go-siwx"
)
func main() {
// Create client with defaults
client, err := siwx.New()
if err != nil {
panic(err)
}
// Generate challenge message
address := "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb"
chain := "eip155:1"
message, err := client.Challenge(address, chain)
if err != nil {
panic(err)
}
fmt.Println("Sign this message:", message)
// After user signs, verify the signature
signature := "0x..." // From user's wallet
session, token, err := client.Verify(signature, address, chain)
if err != nil {
panic(err)
}
fmt.Printf("Authenticated! Token: %s\n", token)
fmt.Printf("Session expires: %v\n", session.Expires)
}client, err := siwx.New(
siwx.WithDomain("example.com"),
siwx.WithChains("eip155:1", "solana:mainnet"),
siwx.WithJWT([]byte("your-secret-key")),
siwx.WithNonceTTL(5*time.Minute),
)import (
"github.com/redis/go-redis/v9"
"github.com/heywinit/go-siwx"
)
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
client, err := siwx.New(
siwx.WithRedis(rdb),
siwx.WithJWT([]byte("secret")),
)package main
import (
"net/http"
"github.com/heywinit/go-siwx"
"github.com/heywinit/go-siwx/handlers"
"github.com/heywinit/go-siwx/middleware"
)
func main() {
client, _ := siwx.New(siwx.WithJWT([]byte("secret")))
mux := http.NewServeMux()
mux.HandleFunc("/challenge", handlers.ChallengeHandler(client))
mux.HandleFunc("/verify", handlers.VerifyHandler(client))
protected := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := middleware.GetSession(r)
w.Write([]byte("Hello, " + session.Address))
})
mux.Handle("/protected", middleware.Auth(client, protected))
http.ListenAndServe(":8080", mux)
}package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/heywinit/go-siwx"
"github.com/heywinit/go-siwx/handlers"
"github.com/heywinit/go-siwx/middleware"
)
func main() {
client, _ := siwx.New(siwx.WithJWT([]byte("secret")))
r := gin.Default()
r.POST("/challenge", gin.WrapH(handlers.ChallengeHandler(client)))
r.POST("/verify", gin.WrapH(handlers.VerifyHandler(client)))
r.GET("/protected", gin.WrapH(middleware.Auth(client, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session := middleware.GetSession(r)
w.Write([]byte("Hello, " + session.Address))
}))))
r.Run(":8080")
}package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/heywinit/go-siwx"
"github.com/heywinit/go-siwx/handlers"
siwxMiddleware "github.com/heywinit/go-siwx/middleware"
)
func main() {
client, _ := siwx.New(siwx.WithJWT([]byte("secret")))
r := chi.NewRouter()
r.Post("/challenge", handlers.ChallengeHandler(client))
r.Post("/verify", handlers.VerifyHandler(client))
r.Group(func(r chi.Router) {
r.Use(func(next http.Handler) http.Handler {
return siwxMiddleware.Auth(client, next)
})
r.Get("/protected", func(w http.ResponseWriter, r *http.Request) {
session := siwxMiddleware.GetSession(r)
w.Write([]byte("Hello, " + session.Address))
})
})
http.ListenAndServe(":8080", r)
}See examples/ directory for more framework examples (Echo, Fiber, etc.).
Creates a new SIWX client. Options:
WithChains(chains ...string)- Set allowed chainsWithRedis(client *redis.Client)- Use Redis for nonce storageWithJWT(secret []byte)- Use JWT tokens with secretWithOpaqueTokens()- Use opaque tokens instead of JWTWithNonceTTL(ttl time.Duration)- Set nonce TTL (default: 5 minutes)WithDomain(domain string)- Set domain for SIWX messages
Generates a challenge message for the given address and chain. Returns the message that should be signed by the user's wallet.
Verifies a signature and returns a session and token. The signature should be from the user signing the challenge message.
Validates a token and returns the associated session.
type Session struct {
Address string // Wallet address
Chain string // Chain identifier (eip155:1, solana:mainnet, etc.)
Expires time.Time // Expiration time
Issued time.Time // Issue time
}| Chain | Identifier | Signature Format |
|---|---|---|
| Ethereum Mainnet | eip155:1 |
EIP-4361 (SIWE) |
| Polygon | eip155:137 |
EIP-4361 (SIWE) |
| Solana Mainnet | solana:mainnet |
SIWS (Phantom std) |
| Solana Devnet | solana:devnet |
SIWS (Phantom std) |
- Per-address/chain/domain nonces - Prevents replay attacks
- 5-minute nonce TTL - Nonces expire quickly
- One-time use nonces - Nonces are deleted after verification
- Signature verification - Cryptographic verification of signatures
- Domain binding - Messages are bound to your domain
- Contract wallet support - Works with Ethereum contract wallets
go test -bench=. -benchmemgo test ./...Coverage target: 90%+
MIT License - see LICENSE file for details.
Contributions welcome! Please open an issue or PR.
heywinit