Autenticazione su Ethereum
Se provieni dallo sviluppo web tradizionale, sei abituato all'accesso con nome utente/password, ai flussi OAuth e ai cookie di sessione. L'autenticazione su Ethereum funziona diversamente e, per molti versi, in modo più semplice.
Su Ethereum, un utente dimostra la propria identità tramite la firma di un messaggio con il proprio portafoglio. Nessuna password da memorizzare. Nessun database di credenziali da cui possano trapelare dati. Solo crittografia.
In cosa differisce dal Web2?
| Web2 | Ethereum |
|---|---|
| Nome utente + password | Indirizzo del portafoglio + firma |
| Il server memorizza le credenziali | L'utente detiene la chiave privata |
| Sessioni gestite da cookie / JWT | Le sessioni iniziano con una firma del portafoglio offchain |
| "Accedi con Google" | "Accedi con Ethereum" |
| Flussi di ripristino della password | Recupero tramite frase seme |
Il cambiamento fondamentale: nel Web2, un server centralizzato ti autentica. Su Ethereum, sei tu ad autenticarti dimostrando di controllare un indirizzo specifico, e chiunque può verificarlo in modo indipendente.
Prerequisiti
Assicurati di aver compreso:
- Gli account di Ethereum e come funzionano
- Cos'è un portafoglio e come connetterne uno
- Le basi della crittografia a chiave pubblica-privata
Come funziona l'autenticazione basata sul portafoglio
Il flusso principale è semplice:
- La tua applicazione decentralizzata (dapp) chiede all'utente di connettere il proprio portafoglio (tramite MetaMask, Rainbow, WalletConnect, ecc.)
- Il portafoglio condivide l'indirizzo Ethereum dell'utente: questo è il suo identificatore pubblico
- La tua dapp genera un messaggio univoco (un nonce o una sfida)
- L'utente firma il messaggio con la propria chiave privata (avviene all'interno del portafoglio)
- Il tuo backend verifica la firma rispetto all'indirizzo dichiarato
- Se valida, l'utente è autenticato
Nessuna password è mai stata digitata, memorizzata o trasmessa.
Accedi con Ethereum (EIP-4361)
EIP-4361 (si apre in una nuova scheda) definisce un formato di messaggio standard per l'accesso a Ethereum, comunemente chiamato SIWE (Sign-In with Ethereum). Sostituisce la firma di messaggi ad-hoc con uno standard strutturato e sicuro.
Un messaggio SIWE si presenta così:
example.com wants you to sign in with your Ethereum account:
0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B
I accept the Terms of Service: https://example.com/tos
URI: https://example.com/login
Version: 1
Chain ID: 1
Nonce: 32891757
Issued At: 2024-06-12T14:30:00Z
Caratteristiche principali di SIWE:
- Vincolo di dominio - il messaggio include il dominio, prevenendo il phishing
- ID della catena - specifica per quale rete è valida la firma
- Nonce - previene gli attacchi di replay
- Scadenza - marca temporale opzionale che limita la finestra di validità
- Risorse - URI opzionali per l'accesso con ambito limitato
Librerie SIWE
- siwe (si apre in una nuova scheda) - Implementazione ufficiale in TypeScript di Spruce
- siwe-rs (si apre in una nuova scheda) - Implementazione in Rust
- siwe-go (si apre in una nuova scheda) - Implementazione in Go
Esempio: accesso lato client con siwe
import { SiweMessage } from 'siwe'
import { BrowserProvider } from 'ethers'
async function signIn() {
const provider = new BrowserProvider(window.ethereum)
const signer = await provider.getSigner()
const address = await signer.getAddress()
// 1. Ottieni un nonce dal tuo backend
const { nonce } = await fetch('/api/auth/nonce').then(r => r.json())
// 2. Crea e firma il messaggio SIWE
const message = new SiweMessage({
domain: window.location.host,
address,
statement: 'Sign in to My Dapp',
uri: window.location.origin,
version: '1',
chainId: 1,
nonce,
})
const signature = await signer.signMessage(message.prepareMessage())
// 3. Invia al backend per la verifica
await fetch('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature }),
})
}
Esempio: verifica lato server (Node.js)
import { SiweMessage, generateNonce } from 'siwe'
// Emetti un nonce e memorizzalo nella sessione in modo che /verify possa controllarlo in seguito
app.get('/api/auth/nonce', (req, res) => {
req.session.nonce = generateNonce()
res.json({ nonce: req.session.nonce })
})
app.post('/api/auth/verify', async (req, res) => {
try {
const { message, signature } = req.body
const siweMessage = new SiweMessage(message)
const { success, data } = await siweMessage.verify({
signature,
nonce: req.session.nonce,
})
if (success) {
// data.address è l'indirizzo Ethereum verificato
// Crea una sessione o un JWT per l'utente
req.session.address = data.address
res.json({ ok: true, address: data.address })
}
} catch {
res.status(401).json({ error: 'Invalid signature' })
}
})
Librerie di connessione del portafoglio
Prima dell'autenticazione, è necessario che l'utente connetta il proprio portafoglio. Queste librerie semplificano l'operazione:
- RainbowKit (si apre in una nuova scheda) - Componente React pronto all'uso con una bellissima interfaccia utente
- ConnectKit (si apre in una nuova scheda) - Modale di connessione del portafoglio pronto all'uso
- AppKit (WalletConnect) (si apre in una nuova scheda) - Connessione del portafoglio multicatena con SIWE integrato
- Wagmi (si apre in una nuova scheda) - Libreria di React Hooks con
useAccount,useConnect
Verificare le firme manualmente
Se preferisci non usare SIWE, puoi verificare le firme direttamente:
import { verifyMessage } from 'ethers'
// Il messaggio che l'utente ha firmato
const message = `Sign in to My Dapp. Nonce: ${storedNonce}`
// Recupera l'indirizzo del firmatario dalla firma
const recoveredAddress = verifyMessage(message, signature)
// Confronta con l'indirizzo dichiarato
if (recoveredAddress.toLowerCase() === claimedAddress.toLowerCase()) {
// Autenticazione riuscita
}
Note importanti sulla sicurezza
- Usa sempre un nonce - previene gli attacchi di replay in cui viene riutilizzata una vecchia firma
- Includi il dominio - impedisce che le firme siano valide su siti diversi
- Controlla la scadenza - le firme dovrebbero avere una finestra di validità limitata
- Usa SIWE (EIP-4361) quando possibile - gestisce tutto quanto sopra per te
- Non esporre mai le chiavi private - la firma avviene all'interno del portafoglio; la tua app vede solo il risultato
Gestione delle sessioni
Una volta autenticato, hai ancora bisogno delle sessioni, proprio come nel Web2. Modelli comuni:
- Token JWT - emetti un JWT dopo aver verificato la firma, da usare per le richieste API
- Sessioni lato server - memorizza l'indirizzo verificato in un cookie di sessione
- SIWE con risorse - definisci token di accesso con ambito limitato collegati a URI specifici
La differenza chiave rispetto al Web2: l'indirizzo Ethereum dell'utente è la sua identità persistente. Può usarlo in qualsiasi dapp senza creare un nuovo account.
Identità decentralizzata
L'autenticazione di Ethereum fa parte di un movimento più ampio verso l'identità auto-sovrana. Gli standard e i progetti in questo spazio includono:
- Ethereum Name Service (ENS) (si apre in una nuova scheda) - Nomi leggibili dall'uomo (es.
vitalik.eth) che si risolvono in indirizzi - Ethereum Attestation Service (EAS) (si apre in una nuova scheda) - Attestazioni onchain su identità e credenziali
- Identificatori decentralizzati (DID) del W3C (si apre in una nuova scheda) - Standard globale per l'identità decentralizzata verificabile
- Ceramic Network (si apre in una nuova scheda) - Flussi di dati decentralizzati legati a un DID
Letture di approfondimento
- EIP-4361: Accedi con Ethereum (si apre in una nuova scheda)
- Documentazione di SIWE (si apre in una nuova scheda)
- Accedi con Ethereum su Auth0 (si apre in una nuova scheda)
- Documentazione sull'autenticazione di Reown AppKit (si apre in una nuova scheda)
- Documentazione di ENS (si apre in una nuova scheda)