0

SIWE Authentication

PreviousNext

A SIWE authentication system with server-side utilities for protecting routes and API endpoints.

Installation

Ensure you have installed and setup the AGW Provider wrapper component first.

1

Install the component

pnpm dlx shadcn@latest add "https://build.abs.xyz/r/siwe-button.json"
2

Generate a secure password

Generate a secure password for iron-session using openssl:

openssl rand -base64 32
3

Add environment variable

Add the password to your .env.local file:

.env.local
IRON_SESSION_PASSWORD="your_secure_password_here"  # Ensure it's wrapped in double quotes

Usage

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 />
}

Using Hooks

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>
  )
}

Server-side Utilities

Included are utilities for checking authentication status in server components and API routes.

Server Component

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>
  );
}
 

API Route

Protect API routes with the requireServerAuth utility.

app/api/protected/route.ts
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 }
    );
  }
}

What's included

This command installs the following files:

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.

"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 });
  }
}