"use client"
import { SiweButton } from "@/components/siwe-button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { useAccount } from "wagmi"
import { useSiweAuthQuery } from "@/hooks/use-siwe-auth-query"
export default function SiweButtonDemo() {
const { isConnected } = useAccount()
const { data: authData, isFetching: isAuthFetching } = useSiweAuthQuery()
const isAuthenticated = authData?.ok && authData?.user?.isAuthenticated
return (
<Card className="w-full gap-0">
<CardHeader className="pb-2">
<CardTitle>Sign in with Ethereum</CardTitle>
<CardDescription>
Authenticate your wallet to access protected features
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-1">
<SiweButton className="w-full" />
{/* Status indicators */}
{isConnected && (
<div className="space-y-2 pt-4 border-t">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Status:</span>
<div className="flex items-center space-x-2">
{isAuthFetching ? (
<>
<div className="h-2 w-2 bg-blue-500 rounded-full animate-pulse" />
<span className="text-sm text-blue-600 dark:text-blue-400">Checking</span>
</>
) : isAuthenticated ? (
<>
<div className="h-2 w-2 bg-green-500 rounded-full" />
<span className="text-sm text-green-600 dark:text-green-400">Authenticated</span>
</>
) : (
<>
<div className="h-2 w-2 bg-yellow-500 rounded-full" />
<span className="text-sm text-yellow-600 dark:text-yellow-400">Connected</span>
</>
)}
</div>
</div>
{authData?.user?.address && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Address:</span>
<span className="text-sm font-mono">
{authData.user.address.slice(0, 6)}...{authData.user.address.slice(-4)}
</span>
</div>
)}
</div>
)}
</CardContent>
</Card>
)
}
Ensure you have installed and setup the AGW Provider wrapper component first.
pnpm dlx shadcn@latest add "https://build.abs.xyz/r/siwe-button.json"
Generate a secure password for iron-session
using openssl
:
openssl rand -base64 32
Add the password to your .env.local
file:
IRON_SESSION_PASSWORD="your_secure_password_here" # Ensure it's wrapped in double quotes
A button that prompts the user to connect their Abstract Global Wallet and sign the SIWE message to authenticate with your application.
import { SiweButton } from "@/components/siwe-button"
export default function App() {
return <SiweButton />
}
Use the authentication states to render content based on the user's authentication status.
"use client"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { ConnectWalletButton } from "@/components/connect-wallet-button"
import { useAccount } from "wagmi"
import { useSiweAuthQuery } from "@/hooks/use-siwe-auth-query"
import { useSiweSignInMutation } from "@/hooks/use-siwe-sign-in-mutation"
import { useSiweLogoutMutation } from "@/hooks/use-siwe-logout-mutation"
export default function SiweButtonDemo() {
const { address, isConnected } = useAccount()
const { data: authData, isLoading: isAuthLoading } = useSiweAuthQuery()
const signInMutation = useSiweSignInMutation()
const logoutMutation = useSiweLogoutMutation()
const isAuthenticated = authData?.ok && authData?.user?.isAuthenticated
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Sign in with Ethereum</CardTitle>
<CardDescription>
Authenticate your wallet to access protected features
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{!isConnected ? (
<ConnectWalletButton className="w-full" />
) : !isAuthenticated ? (
<Button
onClick={() => signInMutation.mutate()}
disabled={signInMutation.isPending || isAuthLoading}
className="w-full"
>
{signInMutation.isPending ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-transparent border-t-current" />
Signing Message...
</>
) : isAuthLoading ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-transparent border-t-current" />
Checking Status...
</>
) : (
"Sign Authentication Message"
)}
</Button>
) : (
<div className="space-y-3">
<Button
onClick={() => logoutMutation.mutate()}
disabled={logoutMutation.isPending}
variant="destructive"
className="w-full"
>
{logoutMutation.isPending ? (
<>
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-transparent border-t-current" />
Signing Out...
</>
) : (
"Sign Out"
)}
</Button>
</div>
)}
{isConnected && (
<div className="space-y-2 pt-4 border-t">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Status:</span>
<div className="flex items-center space-x-2">
{isAuthLoading ? (
<>
<div className="h-2 w-2 bg-blue-500 rounded-full animate-pulse" />
<span className="text-sm text-blue-600 dark:text-blue-400">Checking</span>
</>
) : isAuthenticated ? (
<>
<div className="h-2 w-2 bg-green-500 rounded-full" />
<span className="text-sm text-green-600 dark:text-green-400">Authenticated</span>
</>
) : (
<>
<div className="h-2 w-2 bg-yellow-500 rounded-full" />
<span className="text-sm text-yellow-600 dark:text-yellow-400">Connected</span>
</>
)}
</div>
</div>
{address && (
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Address:</span>
<span className="text-sm font-mono">
{address.slice(0, 6)}...{address.slice(-4)}
</span>
</div>
)}
</div>
)}
</CardContent>
</Card>
)
}
Included are utilities for checking authentication status in server components and API routes.
Gate content to authenticated users in a server component.
import { SiweButton } from "@/components/siwe-button";
import { getServerAuthUser } from "@/lib/auth-server";
export default async function ProtectedPage() {
const auth = await getServerAuthUser();
if (!auth.isAuthenticated) {
return <SiweButton />;
}
return (
<div>
<h1>Welcome!</h1>
<p>Your address: {auth.user?.address}</p>
</div>
);
}
Protect API routes with the requireServerAuth
utility.
import { NextResponse } from "next/server";
import { requireServerAuth } from "@/lib/auth-server";
export async function GET() {
try {
const user = await requireServerAuth();
// User is guaranteed to be authenticated here
// For example, you could fetch user data from a database
// Return some demo user data, or redirect to a protected page
const userData = {
address: user.address,
chainId: user.chainId,
isAuthenticated: user.isAuthenticated,
expirationTime: user.expirationTime,
message: "This is protected data only available to authenticated users",
timestamp: new Date().toISOString()
};
return NextResponse.json(userData);
} catch (error) {
return NextResponse.json(
{ error: error instanceof Error ? error.message : "Authentication required" },
{ status: 401 }
);
}
}
This command installs the following files:
/components
siwe-button.tsx
- A button that prompts the user to connect their Abstract Global Wallet and sign the SIWE message to authenticate with your application./app/api/auth
nonce/route.ts
- API route for generating noncesverify/route.ts
- API route for verifying SIWE messagesuser/route.ts
- API route for getting user auth statuslogout/route.ts
- API route for logging out/config
/hooks
use-siwe-auth-query.ts
- Hook for checking authentication statususe-siwe-sign-in-mutation.ts
- Hook for signing in with SIWEuse-siwe-logout-mutation.ts
- Hook for logging out/lib
auth-server.ts
- Server-side authentication utilities/types
siwe-auth.ts
- TypeScript type definitionssiwe-button.tsx
A button that prompts the user to connect their Abstract Global Wallet and sign the SIWE message to authenticate with your application.
"use client";
import { useAccount } from "wagmi";
import { Button } from "@/components/ui/button";
import { DropdownMenuItem, DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { ConnectWalletButton } from "@/components/connect-wallet-button";
import { useSiweAuthQuery } from "@/hooks/use-siwe-auth-query";
import { useSiweSignInMutation } from "@/hooks/use-siwe-sign-in-mutation";
import { useSiweLogoutMutation } from "@/hooks/use-siwe-logout-mutation";
import { cn } from "@/lib/utils";
import { type ClassValue } from "clsx";
interface SiweButtonProps {
className?: ClassValue;
}
/**
* SIWE Button
*
* A streamlined authentication button that handles:
* - Wallet connection via ConnectWalletButton integration
* - SIWE message signing and verification
* - Authentication state management with balance display
* - Loading states and error handling via toast notifications
*
* States:
* - Not connected: Shows "Connect Wallet" button
* - Connected but not authenticated: Shows "Sign Message" button
* - Authenticated: Shows balance with dropdown containing "Sign Out"
*/
export function SiweButton({ className }: SiweButtonProps) {
const { isConnected } = useAccount();
const { data: authData, isLoading: isAuthLoading } = useSiweAuthQuery();
const signInMutation = useSiweSignInMutation();
const logoutMutation = useSiweLogoutMutation();
// Check if user is authenticated
const isAuthenticated = authData?.ok && authData?.user?.isAuthenticated;
// Handle sign-in action
const handleSignIn = () => {
signInMutation.mutate();
};
// Handle sign-out action (SIWE logout + wallet disconnect)
const handleSignOut = () => {
logoutMutation.mutate();
};
// Not connected: Use ConnectWalletButton
if (!isConnected) {
return <ConnectWalletButton className={className} />;
}
// Connected and authenticated: Use ConnectWalletButton with custom dropdown
if (isConnected && isAuthenticated) {
return (
<ConnectWalletButton
className={className}
customDropdownItems={[
<DropdownMenuSeparator key="sep" />,
<DropdownMenuItem
key="signout"
onClick={handleSignOut}
disabled={logoutMutation.isPending}
className="text-destructive"
>
{logoutMutation.isPending ? (
<>
<Spinner className="mr-2 h-4 w-4 animate-spin" />
Signing out...
</>
) : (
"Sign Out"
)}
</DropdownMenuItem>
]}
/>
);
}
// Connected but not authenticated OR loading: Show Sign Message button
return (
<Button
onClick={handleSignIn}
disabled={signInMutation.isPending || isAuthLoading}
className={cn("cursor-pointer group min-w-40", className)}
>
{signInMutation.isPending ? (
<>
<Spinner className="mr-2 h-4 w-4 animate-spin" />
Signing in...
</>
) : isAuthLoading ? (
<>
<Spinner className="mr-2 h-4 w-4 animate-spin" />
Checking...
</>
) : (
<>
<KeyIcon className="mr-2 h-4 w-4" />
Sign Message
</>
)}
</Button>
);
}
function Spinner({ className }: { className?: ClassValue }) {
return (
<svg
className={cn("animate-spin", className)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="m4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
);
}
function KeyIcon({ className }: { className?: ClassValue }) {
return (
<svg
className={cn(className)}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
/>
</svg>
);
}
use-siwe-auth-query.ts
React Query hook for checking current authentication status.
"use client";
import { useQuery } from "@tanstack/react-query";
import { useAccount } from "wagmi";
import { AuthResponse, ClientSiweConfigurationError } from "@/types/siwe-auth";
async function fetchAuthUser(): Promise<AuthResponse> {
const response = await fetch("/api/auth/user");
const result = await response.json();
// Check for configuration errors and throw proper error type
if (result.isConfigurationError) {
throw new ClientSiweConfigurationError(result.message);
}
return result;
}
export function useSiweAuthQuery() {
const { address, isConnected } = useAccount();
const query = useQuery({
queryKey: ["siwe-auth", address],
queryFn: fetchAuthUser,
// Only run query if wallet is connected
enabled: isConnected && !!address,
// Consider auth data is fresh for 1 minute
staleTime: 1000 * 60 * 1, // 1 minute
// Only refetch on window focus if data is stale (not on every tab switch)
refetchOnWindowFocus: true,
// Always recheck when network reconnects
refetchOnReconnect: true,
// Background recheck every 5 minutes
refetchInterval: 1000 * 60 * 5, // 5 minutes
// Keep auth data in cache for 10 minutes after component unmount
gcTime: 1000 * 60 * 10, // 10 minutes
retry: (failureCount, error: Error & { status?: number }) => {
// Don't retry if it's a 401 (not authenticated)
if (error?.status === 401) {
return false;
}
// Don't retry configuration errors - let them throw
if (error instanceof ClientSiweConfigurationError) {
return false;
}
// Retry up to 2 times for other errors
return failureCount < 2;
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});
// If there's a configuration error, throw it during render to show in Next.js overlay
if (query.error instanceof ClientSiweConfigurationError) {
throw query.error;
}
return query;
}
use-siwe-sign-in-mutation.ts
React Query mutation hook for prompting the user to sign the SIWE message to authenticate with your application. Requires a connected wallet.
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useAccount, useSignMessage } from "wagmi";
import { createSiweMessage } from "viem/siwe";
import { toast } from "sonner";
import {
AuthResponse,
SignInRequest,
ClientSiweConfigurationError,
} from "@/types/siwe-auth";
async function fetchNonce(): Promise<string> {
const response = await fetch("/api/auth/nonce");
// Check if it's a JSON error response (configuration error)
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
const errorData = await response.json();
if (errorData.isConfigurationError) {
// Throw the configuration error to bubble up to Next.js
throw new ClientSiweConfigurationError(errorData.message);
}
throw new Error(errorData.message || "Failed to fetch nonce");
}
return response.text();
}
async function verifySignature(data: SignInRequest): Promise<AuthResponse> {
const response = await fetch("/api/auth/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const result = await response.json();
// Check for configuration errors and throw them to bubble up
if (result.isConfigurationError) {
throw new ClientSiweConfigurationError(result.message);
}
return result;
}
export function useSiweSignInMutation() {
const { address, chainId } = useAccount();
const { signMessageAsync } = useSignMessage();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
if (!address || !chainId) {
throw new Error("Wallet not connected");
}
// Step 1: Fetch nonce
const nonce = await fetchNonce();
// Step 2: Create SIWE message
const message = createSiweMessage({
domain: window.location.host,
address,
statement: "Sign in with Ethereum to the app.",
uri: window.location.origin,
version: "1",
chainId,
nonce,
issuedAt: new Date(),
expirationTime: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 1 week
});
// Step 3: Sign the message
const signature = await signMessageAsync({
message,
});
// Step 4: Verify signature
const result = await verifySignature({
message,
signature: signature as `0x${string}`,
});
if (!result.ok) {
throw new Error(result.message || "Sign-in failed");
}
return result;
},
onSuccess: () => {
// Invalidate auth query to refresh user state
queryClient.invalidateQueries({ queryKey: ["siwe-auth"] });
toast.success("Successfully signed in!");
},
onError: (error: Error) => {
// If it's a configuration error, throw it to show in Next.js overlay
if (error instanceof ClientSiweConfigurationError) {
throw error;
}
// Otherwise handle normally
console.error("Sign-in error:", error);
toast.error(error.message || "Failed to sign in");
},
});
}
use-siwe-logout-mutation.ts
React Query mutation hook for deleting the user's session (signing out).
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useLoginWithAbstract } from "@abstract-foundation/agw-react";
import { toast } from "sonner";
import { ClientSiweConfigurationError } from "@/types/siwe-auth";
interface LogoutResponse {
ok: boolean;
message?: string;
isConfigurationError?: boolean;
}
async function logoutUser(): Promise<LogoutResponse> {
const response = await fetch("/api/auth/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
// Check for configuration errors and throw proper error type
if (result.isConfigurationError) {
throw new ClientSiweConfigurationError(result.message);
}
if (!response.ok) {
throw new Error(result.message || "Logout failed");
}
return result;
}
/**
* React Query mutation hook for SIWE logout functionality.
* Handles the logout process and updates auth state.
*
* @returns UseMutationResult for logout operation
*
* @example
* ```tsx
* import { useSiweLogoutMutation } from "@/hooks/use-siwe-logout-mutation";
*
* function LogoutButton() {
* const logoutMutation = useSiweLogoutMutation();
*
* return (
* <button
* onClick={() => logoutMutation.mutate()}
* disabled={logoutMutation.isPending}
* >
* {logoutMutation.isPending ? "Signing out..." : "Sign Out"}
* </button>
* );
* }
* ```
*
* @example
* ```tsx
* // With custom success/error handling
* function MyComponent() {
* const logoutMutation = useSiweLogoutMutation({
* onSuccess: () => {
* // Custom success handling
* router.push("/");
* },
* onError: (error) => {
* // Custom error handling
* console.error("Logout failed:", error);
* }
* });
*
* return (
* <button onClick={() => logoutMutation.mutate()}>
* Sign Out
* </button>
* );
* }
* ```
*/
export function useSiweLogoutMutation(options?: {
onSuccess?: () => void;
onError?: (error: Error) => void;
}) {
const queryClient = useQueryClient();
const { logout: walletLogout } = useLoginWithAbstract();
const mutation = useMutation({
mutationFn: logoutUser,
onSuccess: (data) => {
// Immediately reset auth query data to logged out state
queryClient.setQueryData(["siwe-auth"], {
ok: false,
message: "Logged out",
});
// Also invalidate to trigger refetch
queryClient.invalidateQueries({ queryKey: ["siwe-auth"] });
// Disconnect wallet for complete logout experience
walletLogout();
// Show success toast
toast.success("Successfully signed out");
// Call custom success handler if provided
options?.onSuccess?.();
},
onError: (error: Error) => {
// Don't show error toast for configuration errors - they should be thrown
if (error instanceof ClientSiweConfigurationError) {
throw error;
}
// Show error toast for other errors
toast.error(error.message || "Sign out failed");
// Call custom error handler if provided
options?.onError?.(error);
},
});
// If there's a configuration error, throw it during render to show in Next.js overlay
if (mutation.error instanceof ClientSiweConfigurationError) {
throw mutation.error;
}
return mutation;
}
types/siwe-auth.ts
TypeScript type definitions for SIWE authentication including session data, authentication responses, and configuration error handling.
export interface SessionData {
nonce?: string;
isAuthenticated?: boolean;
address?: `0x${string}`;
chainId?: number;
expirationTime?: string;
}
export interface AuthUser {
isAuthenticated: boolean;
address: `0x${string}`;
chainId?: number;
expirationTime?: string;
}
export interface AuthResponse {
ok: boolean;
message?: string;
user?: AuthUser;
isConfigurationError?: boolean;
}
export interface ConfigurationErrorResponse {
ok: false;
isConfigurationError: true;
message: string;
}
/**
* Client-side configuration error that matches the server-side SiweConfigurationError
*/
export class ClientSiweConfigurationError extends Error {
constructor(message: string) {
super(message);
this.name = "SiweConfigurationError";
}
}
export interface SignInRequest {
message: string;
signature: `0x${string}`;
}
auth.ts
A config file for Iron Session options.
import { SessionOptions } from "iron-session";
/**
* Custom error class for SIWE authentication configuration issues.
* These errors bubble up to show helpful messages.
*/
export class SiweConfigurationError extends Error {
constructor(message: string) {
super(message);
this.name = "SiweConfigurationError";
}
}
/**
* Validates and returns the Iron Session password from environment variables.
* Throws a SiweConfigurationError if not properly configured.
*/
function getSessionPassword(): string {
const password = process.env.IRON_SESSION_PASSWORD;
if (!password) {
throw new SiweConfigurationError(
"IRON_SESSION_PASSWORD environment variable is required for SIWE authentication.\n\n" +
"This password is used to encrypt session data and must be cryptographically secure.\n\n" +
"To fix this:\n" +
"1. Generate a secure password: openssl rand -base64 32\n" +
"2. Add it to your .env.local file:\n" +
' IRON_SESSION_PASSWORD="your_generated_password_here"\n' +
"3. Restart your application\n\n" +
"SECURITY WARNING: Never use a weak or default password in production!"
);
}
if (password.length < 32) {
throw new SiweConfigurationError(
"IRON_SESSION_PASSWORD must be at least 32 characters long for security.\n" +
"Generate a new secure password using: openssl rand -base64 32"
);
}
return password;
}
/**
* Gets Iron Session configuration for SIWE authentication.
* Validates IRON_SESSION_PASSWORD on first call.
*/
export function getIronOptions(): SessionOptions {
return {
password: getSessionPassword(),
cookieName: "siwe-session",
cookieOptions: {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "strict",
maxAge: 60 * 60 * 24 * 7, // 7 days
},
};
}
auth-server.ts
Server-side authentication utilities for safely checking authentication state in API routes, Server Components, and middleware. See Server-side Utilities for more details and examples.
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import { SessionData } from "@/app/api/auth/nonce/route";
import { getIronOptions, SiweConfigurationError } from "@/config/auth";
import { chain } from "@/config/chain";
import { AuthUser } from "./types";
/**
* Server-side authentication utilities for SIWE (Sign-in with Ethereum).
* These functions provide safe and convenient ways to check authentication state
* on the server-side in API routes, Server Components, and middleware.
*/
export interface ServerAuthResult {
isAuthenticated: boolean;
user?: AuthUser;
error?: string;
}
/**
* Safely retrieves the current authenticated user from the server-side session.
* Returns null if not authenticated or if there's an error.
*
* @returns Promise<ServerAuthResult> - Authentication result with user data or error
*
* @example
* ```tsx
* // In a Server Component
* import { getServerAuthUser } from "@/lib/auth-server";
*
* export default async function ProtectedPage() {
* const auth = await getServerAuthUser();
*
* if (!auth.isAuthenticated) {
* return <div>Please sign in to access this page</div>;
* }
*
* return (
* <div>
* <h1>Welcome!</h1>
* <p>Your address: {auth.user?.address}</p>
* </div>
* );
* }
* ```
*
* @example
* ```tsx
* // In an API route
* import { getServerAuthUser } from "@/lib/auth-server";
*
* export async function GET() {
* const auth = await getServerAuthUser();
*
* if (!auth.isAuthenticated) {
* return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
* }
*
* // Access protected data
* const userData = await getUserData(auth.user!.address);
* return NextResponse.json(userData);
* }
* ```
*/
export async function getServerAuthUser(): Promise<ServerAuthResult> {
try {
const session = await getIronSession<SessionData>(
await cookies(),
getIronOptions()
);
// Check if user is authenticated
if (!session.isAuthenticated || !session.address) {
return {
isAuthenticated: false,
error: "No user session found"
};
}
// Check if session is expired
if (
session.expirationTime &&
new Date(session.expirationTime).getTime() < Date.now()
) {
return {
isAuthenticated: false,
error: "SIWE session expired"
};
}
// Check if chain matches
if (session.chainId !== chain.id) {
return {
isAuthenticated: false,
error: "Invalid chain"
};
}
// Return authenticated user data
return {
isAuthenticated: true,
user: {
isAuthenticated: session.isAuthenticated,
address: session.address,
chainId: session.chainId,
expirationTime: session.expirationTime,
}
};
} catch (error) {
// Handle configuration errors
if (error instanceof SiweConfigurationError) {
return {
isAuthenticated: false,
error: `Configuration error: ${error.message}`
};
}
// Handle unexpected errors
return {
isAuthenticated: false,
error: "Authentication check failed"
};
}
}
/**
* Requires authentication and throws an error if not authenticated.
* Useful for API routes that need to ensure authentication.
*
* @returns Promise<AuthUser> - The authenticated user data
* @throws Error if not authenticated
*
* @example
* ```tsx
* // In an API route
* import { requireServerAuth } from "@/lib/auth-server";
*
* export async function POST(request: Request) {
* try {
* const user = await requireServerAuth();
*
* // User is guaranteed to be authenticated here
* const result = await performProtectedAction(user.address);
* return NextResponse.json(result);
*
* } catch (error) {
* return NextResponse.json(
* { error: error.message },
* { status: 401 }
* );
* }
* }
* ```
*/
export async function requireServerAuth(): Promise<AuthUser> {
const auth = await getServerAuthUser();
if (!auth.isAuthenticated || !auth.user) {
throw new Error(auth.error || "Authentication required");
}
return auth.user;
}
/**
* Checks if a user is authenticated (boolean check only).
* Useful for conditional rendering or simple auth checks.
*
* @returns Promise<boolean> - True if authenticated, false otherwise
*
* @example
* ```tsx
* // In a Server Component
* import { isServerAuthenticated } from "@/lib/auth-server";
*
* export default async function HomePage() {
* const isAuthenticated = await isServerAuthenticated();
*
* return (
* <div>
* {isAuthenticated ? (
* <p>Welcome back!</p>
* ) : (
* <p>Please sign in</p>
* )}
* </div>
* );
* }
* ```
*/
export async function isServerAuthenticated(): Promise<boolean> {
const auth = await getServerAuthUser();
return auth.isAuthenticated;
}
/**
* Gets the authenticated user's address safely.
* Returns null if not authenticated.
*
* @returns Promise<string | null> - The user's address or null
*
* @example
* ```tsx
* // In an API route
* import { getServerAuthAddress } from "@/lib/auth-server";
*
* export async function GET() {
* const address = await getServerAuthAddress();
*
* if (!address) {
* return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
* }
*
* const balance = await getBalance(address);
* return NextResponse.json({ address, balance });
* }
* ```
*/
export async function getServerAuthAddress(): Promise<`0x${string}` | null> {
const auth = await getServerAuthUser();
return auth.user?.address || null;
}
app/api/auth/nonce/route.ts
API route for generating unique nonces required for SIWE message creation.
// Hello world
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { generateSiweNonce } from "viem/siwe";
import { getIronSession } from "iron-session";
import { getIronOptions, SiweConfigurationError } from "@/config/auth";
export interface SessionData {
nonce?: string;
isAuthenticated?: boolean;
address?: `0x${string}`;
chainId?: number;
expirationTime?: string;
}
/**
* Sign in with Ethereum - Generate a unique nonce for the SIWE message.
*/
export async function GET() {
try {
// The "session" here is not related to our session keys.
// This is just related to auth / sign in with Ethereum.
const session = await getIronSession<SessionData>(
await cookies(),
getIronOptions()
);
// Generate and store the nonce
const nonce = generateSiweNonce();
session.nonce = nonce;
await session.save();
// Return the nonce as plain text with no-cache headers
return new NextResponse(nonce, {
headers: {
"Cache-Control":
"no-store, no-cache, must-revalidate, proxy-revalidate",
Pragma: "no-cache",
Expires: "0",
},
});
} catch (error) {
// Return configuration errors as special response type
if (error instanceof SiweConfigurationError) {
return NextResponse.json(
{
ok: false,
isConfigurationError: true,
message: error.message,
},
{ status: 500 }
);
}
// Catch other unexpected errors
return NextResponse.json({ ok: false }, { status: 500 });
}
}
app/api/auth/verify/route.ts
API route for verifying SIWE messages with session validation and expiration checking.
import { NextRequest, NextResponse } from "next/server";
import { parseSiweMessage } from "viem/siwe";
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import { SessionData } from "../nonce/route";
import { createPublicClient, http } from "viem";
import { getIronOptions, SiweConfigurationError } from "@/config/auth";
import { chain } from "@/config/chain";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { message, signature } = body;
// Validate required fields
if (!message || !signature) {
return NextResponse.json(
{ ok: false, message: "Message and signature are required." },
{ status: 400 }
);
}
// Validate message is a string
if (typeof message !== 'string') {
return NextResponse.json(
{ ok: false, message: "Message must be a string." },
{ status: 400 }
);
}
// Validate signature is a valid hex string (supports both EOA and EIP-1271 signatures)
if (typeof signature !== 'string' || !/^0x[a-fA-F0-9]+$/.test(signature) || signature.length < 4) {
return NextResponse.json(
{ ok: false, message: "Invalid signature format." },
{ status: 400 }
);
}
// The "session" here is not related to our session keys.
// This is just related to auth / sign in with Ethereum.
const session = await getIronSession<SessionData>(
await cookies(),
getIronOptions()
);
const publicClient = createPublicClient({
chain,
transport: http(),
});
try {
// Validate nonce exists
if (!session.nonce) {
return NextResponse.json(
{ ok: false, message: "No nonce found. Please request a new nonce first." },
{ status: 422 }
);
}
// Parse and validate SIWE message before signature verification
const siweMessage = parseSiweMessage(message);
// Validate chain ID matches expected chain
if (siweMessage.chainId !== chain.id) {
return NextResponse.json(
{ ok: false, message: "Invalid chain ID." },
{ status: 422 }
);
}
// Validate domain matches current host to prevent cross-domain replay attacks
const requestHost = request.headers.get("host");
if (siweMessage.domain !== requestHost) {
return NextResponse.json(
{ ok: false, message: "Invalid domain." },
{ status: 422 }
);
}
// Validate message expiration time
if (siweMessage.expirationTime && siweMessage.expirationTime.getTime() <= Date.now()) {
return NextResponse.json(
{ ok: false, message: "Message has expired." },
{ status: 422 }
);
}
// Create and verify the SIWE message (with EIP-1271 support for smart contract wallets)
const valid = await publicClient.verifySiweMessage({
message,
signature: signature as `0x${string}`,
nonce: session.nonce,
blockTag: 'latest', // EIP-1271 smart contract wallet support
});
// Clear nonce after any verification attempt to prevent reuse
session.nonce = undefined;
// If verification is successful, update the auth state
if (valid) {
session.isAuthenticated = true;
session.address = siweMessage.address as `0x${string}`;
session.chainId = siweMessage.chainId;
session.expirationTime = siweMessage.expirationTime?.toISOString();
await session.save();
} else {
// Save session to persist nonce clearing even on failure
await session.save();
}
if (!valid) {
return NextResponse.json(
{ ok: false, message: "Invalid signature." },
{ status: 422 }
);
}
} catch {
return NextResponse.json(
{ ok: false, message: "Verification failed" },
{ status: 500 }
);
}
return NextResponse.json({ ok: true });
} catch (error) {
// Return configuration errors as special response type
if (error instanceof SiweConfigurationError) {
return NextResponse.json({
ok: false,
isConfigurationError: true,
message: error.message
}, { status: 500 });
}
// Catch other unexpected errors
return NextResponse.json({ ok: false }, { status: 500 });
}
}
app/api/auth/user/route.ts
API route for retrieving current authenticated user information with session validation and expiration checking.
import { NextResponse } from "next/server";
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import { SessionData } from "../nonce/route";
import { getIronOptions, SiweConfigurationError } from "@/config/auth";
import { chain } from "@/config/chain";
/**
* Sign in with Ethereum - Get the currently authenticated user information.
* @returns
*/
export async function GET() {
try {
// The "session" here is not related to our session keys.
// This is just related to auth / sign in with Ethereum.
const session = await getIronSession<SessionData>(
await cookies(),
getIronOptions()
);
if (!session.isAuthenticated || !session.address) {
return NextResponse.json(
{ ok: false, message: "No user session found." },
{ status: 401 }
);
}
if (
session.expirationTime &&
new Date(session.expirationTime).getTime() < Date.now()
) {
return NextResponse.json(
{ ok: false, message: "SIWE session expired." },
{ status: 401 }
);
}
if (session.chainId !== chain.id) {
return NextResponse.json(
{ ok: false, message: "Invalid chain." },
{ status: 401 }
);
}
// Return the SIWE session data
return NextResponse.json({
ok: true,
user: {
isAuthenticated: session.isAuthenticated,
address: session.address,
chainId: session.chainId,
expirationTime: session.expirationTime,
},
});
} catch (error) {
// Return configuration errors as special response type
if (error instanceof SiweConfigurationError) {
return NextResponse.json({
ok: false,
isConfigurationError: true,
message: error.message
}, { status: 500 });
}
// Catch other unexpected errors
return NextResponse.json({ ok: false }, { status: 500 });
}
}
app/api/auth/logout/route.ts
API route for handling logout functionality with complete session cleanup and destruction.
import { NextResponse } from "next/server";
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import { SessionData } from "../nonce/route";
import { getIronOptions, SiweConfigurationError } from "@/config/auth";
/**
* Sign in with Ethereum - Logout and destroy the current session.
*/
export async function POST() {
try {
const session = await getIronSession<SessionData>(
await cookies(),
getIronOptions()
);
// Clear all session data
session.isAuthenticated = false;
session.address = undefined;
session.chainId = undefined;
session.expirationTime = undefined;
session.nonce = undefined;
// Destroy the session
session.destroy();
return NextResponse.json({ ok: true, message: "Successfully logged out" });
} catch (error) {
// Let configuration errors bubble up to show helpful messages
if (error instanceof SiweConfigurationError) {
throw error;
}
// Catch other unexpected errors
return NextResponse.json({ ok: false }, { status: 500 });
}
}
On This Page
InstallationUsageUsing HooksServer-side UtilitiesServer ComponentAPI RouteWhat's includedsiwe-button.tsx
use-siwe-auth-query.ts
use-siwe-sign-in-mutation.ts
use-siwe-logout-mutation.ts
types/siwe-auth.ts
auth.ts
auth-server.ts
app/api/auth/nonce/route.ts
app/api/auth/verify/route.ts
app/api/auth/user/route.ts
app/api/auth/logout/route.ts