Inicio de sesión seguro con Ethereum en Next.js: guía paso a paso con el AppKit de WalletConnect

Contar con métodos de autenticación seguros y fáciles de usar es clave para incorporar a más usuarios a la web3. Sign In With Ethereum (SIWE) se ha consolidado como una solución potente que permite a los usuarios controlar su identidad digital con sus cuentas de Ethereum. El nuevo AppKit de WalletConnect hace más fácil que nunca integrar en tus aplicaciones el inicio de sesión social (Social Login), soluciones de on-ramp y SIWE. Este artículo te guiará en la implementación de SIWE en tu aplicación Next.js con la función "One Click Auth" del AppKit de WalletConnect, con especial atención a la protección de las rutas de API mediante tokens JWT.

Entender SIWE y AppKit

Sign In With Ethereum (SIWE) es un método de autenticación estandarizado (EIP-4361) que permite a los usuarios demostrar que son propietarios de su dirección de Ethereum mediante una firma criptográfica. El AppKit de WalletConnect simplifica este proceso al ofrecer a los desarrolladores una integración optimizada.

La función "One-Click Auth" de AppKit supone un avance importante en la experiencia de usuario: permite conectar la wallet y firmar un mensaje SIWE con un solo clic. Este proceso fluido no solo refuerza la seguridad, sino que también mejora la experiencia de usuario general de las aplicaciones descentralizadas (dApps).

Configuración inicial

Para empezar, primero tendrás que instalar correctamente el AppKit de WalletConnect siguiendo este tutorial. Después ya podemos configurar la función "One Click Auth"; ejecuta este comando en tu proyecto Next.js:

yarn add @web3modal/siwe next-auth

Configurar el cliente SIWE

Vamos a crear el cliente SIWE. Crea un archivo en app/config/siwe.ts y copia y pega este código:

app/config/siwe.ts
import { getCsrfToken, signIn, signOut, getSession } from 'next-auth/react'
import type { SIWEVerifyMessageArgs, SIWECreateMessageArgs, SIWESession } from '@web3modal/siwe'
import { createSIWEConfig, formatMessage } from '@web3modal/siwe'
import { mainnet, sepolia } from 'viem/chains'

export const siweConfig = createSIWEConfig({
  getMessageParams: async () => ({
    domain: typeof window !== 'undefined' ? window.location.host : '',
    uri: typeof window !== 'undefined' ? window.location.origin : '',
    chains: [mainnet.id, sepolia.id],
    statement: 'Please sign with your account'
  }),
  createMessage: ({ address, ...args }: SIWECreateMessageArgs) => formatMessage(args, address),
  getNonce: async () => {
    const nonce = await getCsrfToken()
    if (!nonce) {
      throw new Error('Failed to get nonce!')
    }

    return nonce
  },
  getSession: async () => {
    const session = await getSession()
    if (!session) {
      throw new Error('Failed to get session!')
    }

    const { address, chainId } = session as unknown as SIWESession

    return { address, chainId }
  },
  verifyMessage: async ({ message, signature }: SIWEVerifyMessageArgs) => {
    try {
      const success = await signIn('credentials', {
        message,
        redirect: false,
        signature,
        callbackUrl: '/protected'
      })

      return Boolean(success?.ok)
    } catch (error) {
      return false
    }
  },
  signOut: async () => {
    try {
      await signOut({
        redirect: false
      })

      return true
    } catch (error) {
      return false
    }
  }
})

Esta configuración define los parámetros básicos de los mensajes SIWE y se encarga de generar el nonce. Añade la configuración a tu objeto createWeb3Modal y ¡listo!

Web3Provider.ts
import { siweConfig } from '@/config/siwe'

createWeb3Modal({
  //...
  siweConfig
})

Después de conectarse, los usuarios verán una solicitud para firmar un mensaje con su wallet:

siwe walletconnect
La ventana modal 'One Click Auth' de WalletConnect es muy fácil de configurar

Configurar la ruta de API

A continuación, crea una ruta de API para gestionar la autenticación. Crea un archivo en app/api/auth/[...nextauth]/route.ts. No olvides definir NEXTAUTH_SECRET y NEXT_PUBLIC_PROJECT_ID en tu archivo .env.local. Te recomiendo usar una herramienta para generar contraseñas para tu frase secreta.

api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth'
import credentialsProvider from 'next-auth/providers/credentials'
import {
  type SIWESession,
  verifySignature,
  getChainIdFromMessage,
  getAddressFromMessage,
} from '@web3modal/siwe'

declare module 'next-auth' {
  interface Session extends SIWESession {
    address: string
    chainId: number
  }
}

const nextAuthSecret = process.env.NEXTAUTH_SECRET
if (!nextAuthSecret) {
  throw new Error('NEXTAUTH_SECRET is not set')
}

const projectId = process.env.NEXT_PUBLIC_PROJECT_ID
if (!projectId) {
  throw new Error('NEXT_PUBLIC_PROJECT_ID is not set')
}

const providers = [
  credentialsProvider({
    name: 'Base',
    credentials: {
      message: {
        label: 'Message',
        type: 'text',
        placeholder: '0x0',
      },
      signature: {
        label: 'Signature',
        type: 'text',
        placeholder: '0x0',
      },
    },
    async authorize(credentials) {
      try {
        if (!credentials?.message) {
          throw new Error('SiweMessage is undefined')
        }
        const { message, signature } = credentials
        const address = getAddressFromMessage(message)
        const chainId = getChainIdFromMessage(message)

        const isValid = await verifySignature({ address, message, signature, chainId, projectId })

        if (isValid) {
          return {
            id: `${chainId}:${address}`,
          }
        }

        return null
      } catch (e) {
        return null
      }
    },
  }),
]

const handler = NextAuth({
  // https://next-auth.js.org/configuration/providers/oauth
  secret: nextAuthSecret,
  providers,
  session: {
    strategy: 'jwt',
  },
  callbacks: {
    session({ session, token }) {
      if (!token.sub) {
        return session
      }

      const [, chainId, address] = token.sub.split(':')
      if (chainId && address) {
        session.address = address
        session.chainId = parseInt(chainId, 10)
      }

      return session
    },
  },
})

export { handler as GET, handler as POST }

Esta configuración crea un handler de NextAuth que usa SIWE para la autenticación: verifica la firma y, si la autenticación tiene éxito, crea una sesión (con un token JWT).

Proteger las rutas de API con un middleware

Ahora que tenemos SIWE configurado, vamos a proteger nuestras rutas de API con los tokens JWT de next-auth. Podemos aprovechar getToken() en un middleware de Next.js para verificar si el token de next-auth es válido, obtener la dirección de la wallet del usuario y pasársela a la ruta de API:

src/middleware.ts
import { getToken } from 'next-auth/jwt'
import { NextResponse, type NextRequest } from 'next/server'

// List routes you want to protect here
export const config = {
  matcher: ['/api/events/:path*', '/api/users/:path*'],
}

export async function middleware(req: NextRequest) {
  //get wallet id from session
  const token = await getToken({ req })

  if (!token) {
    return NextResponse.json({ success: false, message: 'Authentication failed' }, { status: 401 })
  }

  //get wallet id from session token
  const [, chainId, address] = token.sub.split(':')

  // set user wallet address in the request headers
  const requestHeaders = new Headers(req.headers)
  requestHeaders.set('wallet-address', address)

  return NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })
}

Este middleware protegerá automáticamente las rutas especificadas y exigirá autenticación para acceder a ellas.

Conclusión

Implementar Sign In With Ethereum (SIWE) con el AppKit de WalletConnect en tu aplicación Next.js te proporciona un método de autenticación seguro y fácil de usar. Al aprovechar los tokens JWT de next-auth para proteger las rutas de API, creas un sistema robusto que protege los datos de los usuarios y, al mismo tiempo, ofrece las ventajas de la autenticación Web3.

Recuerda mantener siempre al día tus mecanismos de autenticación y seguir las buenas prácticas de seguridad. Con SIWE y una protección adecuada de las rutas de API, vas por buen camino para construir aplicaciones descentralizadas seguras y atractivas. ¡Feliz programación, y te damos la bienvenida al futuro de la autenticación Web3!

Más artículos

¿Quieres llevar tu proyecto al siguiente nivel?

Contáctanos hoy para ver cómo podemos ayudarte a alcanzar tus objetivos en el sector blockchain.