Sign In With Ethereum (SIWE)
Sign in with Ethereum (SIWE) 플러그인은 사용자가 ERC-4361 표준을 따르는 Ethereum 지갑을 사용하여 인증할 수 있도록 합니다. 이 플러그인은 자체 메시지 검증 및 논스(nonce) 생성 로직을 구현할 수 있는 유연성을 제공합니다.
설치
서버 플러그인 추가
auth 구성에 SIWE 플러그인을 추가하세요:
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
siwe({
domain: "example.com",
emailDomainName: "example.com", // 선택 사항
anonymous: false, // 선택 사항, 기본값은 true
getNonce: async () => {
// 여기에 논스 생성 로직을 구현하세요
return "your-secure-random-nonce";
},
verifyMessage: async (args) => {
// 여기에 SIWE 메시지 검증 로직을 구현하세요
// 메시지에 대해 서명을 검증해야 합니다
return true; // 서명이 유효한 경우 true 반환
},
ensLookup: async (args) => {
// 선택 사항: 사용자 이름과 아바타에 대한 ENS 조회 구현
return {
name: "user.eth",
avatar: "https://example.com/avatar.png"
};
},
}),
],
});데이터베이스 마이그레이션
마이그레이션을 실행하거나 스키마를 생성하여 필요한 필드와 테이블을 데이터베이스에 추가하세요.
npx @better-auth/cli migratenpx @better-auth/cli generate수동으로 필드를 추가하려면 스키마 섹션을 참조하세요.
클라이언트 플러그인 추가
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [siweClient()],
});사용법
논스(Nonce) 생성
SIWE 메시지에 서명하기 전에 지갑 주소에 대한 논스를 생성해야 합니다:
const { data, error } = await authClient.siwe.nonce({
walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
chainId: 1, // Ethereum 메인넷의 경우 선택 사항, 다른 체인의 경우 필수. 기본값은 1
});
if (data) {
console.log("Nonce:", data.nonce);
}Sign In with Ethereum
논스를 생성하고 SIWE 메시지를 만든 후 서명을 검증하여 인증하세요:
const { data, error } = await authClient.siwe.verify({
message: "Your SIWE message string",
signature: "0x...", // 사용자 지갑의 서명
walletAddress: "0x1234567890abcdef1234567890abcdef12345678",
chainId: 1, // Ethereum 메인넷의 경우 선택 사항, 다른 체인의 경우 필수. SIWE 메시지의 Chain ID와 일치해야 함
email: "user@example.com", // 선택 사항, anonymous가 false인 경우 필수
});
if (data) {
console.log("인증 성공:", data.user);
}체인별 예제
다양한 블록체인 네트워크에 대한 예제입니다:
// Ethereum 메인넷 (chainId 생략 가능, 기본값 1)
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
// chainId: 1 (기본값)
});// Polygon (chainId 필수)
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 137, // Polygon에 필수
});// Arbitrum (chainId 필수)
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 42161, // Arbitrum에 필수
});// Base (chainId 필수)
const { data, error } = await authClient.siwe.verify({
message,
signature,
walletAddress,
chainId: 8453, // Base에 필수
});chainId는 SIWE 메시지에 지정된 Chain ID와 일치해야 합니다. 메시지의 Chain ID와 chainId 매개변수 간에 불일치가 있으면 401 오류와 함께 검증이 실패합니다.
구성 옵션
서버 옵션
SIWE 플러그인은 다음 구성 옵션을 허용합니다:
- domain: 애플리케이션의 도메인 이름 (SIWE 메시지 생성에 필수)
- emailDomainName: 익명 모드를 사용하지 않을 때 사용자 계정을 생성하기 위한 이메일 도메인 이름. 기본값은 base URL의 도메인
- anonymous: 이메일을 요구하지 않고 익명 로그인을 허용할지 여부. 기본값은
true - getNonce: 각 로그인 시도에 대해 고유한 논스를 생성하는 함수. 암호학적으로 안전한 무작위 문자열을 반환하도록 이 함수를 구현해야 합니다.
Promise<string>을 반환해야 함 - verifyMessage: 서명된 SIWE 메시지를 검증하는 함수. 메시지 세부 정보를 받아
Promise<boolean>을 반환해야 함 - ensLookup: Ethereum 주소에 대한 ENS 이름과 아바타를 조회하는 선택적 함수
클라이언트 옵션
SIWE 클라이언트 플러그인은 구성 옵션이 필요하지 않지만, 향후 확장성을 위해 필요한 경우 전달할 수 있습니다:
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [
siweClient({
// 선택적 클라이언트 구성을 여기에 추가할 수 있습니다
}),
],
});스키마
SIWE 플러그인은 사용자 지갑 연결을 저장하기 위한 walletAddress 테이블을 추가합니다:
| 필드 | 타입 | 설명 |
|---|---|---|
| id | string | 기본 키 |
| userId | string | user.id 참조 |
| address | string | Ethereum 지갑 주소 |
| chainId | number | Chain ID (예: Ethereum 메인넷의 경우 1) |
| isPrimary | boolean | 사용자의 주 지갑 여부 |
| createdAt | date | 생성 타임스탬프 |
구현 예제
SIWE 인증을 구현하는 방법을 보여주는 완전한 예제입니다:
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";
import { generateRandomString } from "better-auth/crypto";
import { verifyMessage, createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
export const auth = betterAuth({
database: {
// 데이터베이스 구성
},
plugins: [
siwe({
domain: "myapp.com",
emailDomainName: "myapp.com",
anonymous: false,
getNonce: async () => {
// 암호학적으로 안전한 무작위 논스 생성
return generateRandomString(32);
},
verifyMessage: async ({ message, signature, address }) => {
try {
// viem을 사용하여 서명 검증 (권장)
const isValid = await verifyMessage({
address: address as `0x${string}`,
message,
signature: signature as `0x${string}`,
});
return isValid;
} catch (error) {
console.error("SIWE 검증 실패:", error);
return false;
}
},
ensLookup: async ({ walletAddress }) => {
try {
// 선택 사항: viem을 사용하여 ENS 이름과 아바타 조회
// 여기에서 viem의 ENS 유틸리티를 사용할 수 있습니다
const client = createPublicClient({
chain: mainnet,
transport: http(),
});
const ensName = await client.getEnsName({
address: walletAddress as `0x${string}`,
});
const ensAvatar = ensName
? await client.getEnsAvatar({
name: ensName,
})
: null;
return {
name: ensName || walletAddress,
avatar: ensAvatar || "",
};
} catch {
return {
name: walletAddress,
avatar: "",
};
}
},
}),
],
});