A complete, self-contained Bitcoin wallet authentication system for Next.js applications. This system provides secure, cryptographic authentication using Bitcoin wallet signatures with address whitelisting.
- 🔐 Cryptographic Authentication: Users sign challenge messages with their Bitcoin wallet
- 🛡️ Address Whitelisting: Only authorized Bitcoin addresses can access your application
- 🔒 Secure Sessions: Server-side session management with HttpOnly cookies
- 🚪 Route Protection: Automatic middleware-based route protection
- 💼 Multi-Wallet Support: Compatible with UniSat, Xverse, and OKX wallets
- 📊 Supabase Integration: Database-backed whitelist management
1. User visits protected page
2. Middleware redirects to /login
3. User connects Bitcoin wallet (UniSat, Xverse, OKX)
4. System generates unique challenge message
5. User signs challenge with wallet private key
6. Server verifies signature and checks whitelist
7. If valid, creates secure session and redirects to app
- Challenge-Response Authentication: Unique challenge messages prevent replay attacks
- Server-Side Verification: All authentication logic runs on the server
- Secure Cookies: HttpOnly cookies prevent client-side manipulation
- Row Level Security: Database policies prevent unauthorized access
- Session Expiration: Sessions expire after 24 hours
See SETUP.md for detailed installation instructions.
auth/
├── README.md # This file
├── SETUP.md # Installation guide
├── components/
│ ├── auth-guard.tsx # Client-side auth guard component
│ └── wallet-connector.tsx # Wallet connection UI component
├── lib/
│ ├── auth.ts # Server-side auth functions
│ ├── auth-utils.ts # Client-side auth utilities
│ └── supabase/
│ ├── client.ts # Supabase client setup
│ └── server.ts # Supabase server setup
├── api/
│ └── auth/
│ ├── challenge/route.ts # Generate challenge message
│ ├── verify/route.ts # Verify signature and create session
│ ├── session/route.ts # Check session status
│ └── logout/route.ts # Clear session
├── pages/
│ └── login/page.tsx # Login page UI
├── middleware.ts # Route protection middleware
└── scripts/
├── 002_create_whitelist_table.sql # Database schema
└── 003_add_whitelist_addresses.sql # Add authorized addresses
GET /api/auth/challenge- Generate a unique challenge message for signingPOST /api/auth/verify- Verify wallet signature and create authentication sessionGET /api/auth/session- Check current session statusPOST /api/auth/logout- Clear session and logout user
// Request
GET /api/auth/challenge
// Response
{
"success": true,
"challenge": "Bitcoin PSBT Builder Authentication\nTimestamp: 1234567890\nNonce: abc123..."
}// Request
POST /api/auth/verify
{
"address": "bc1q...",
"message": "Bitcoin PSBT Builder Authentication...",
"signature": "base64_signature..."
}
// Response
{
"success": true,
"message": "Authentication successful",
"address": "bc1q..."
}// app/dashboard/page.tsx
import { AuthGuard } from "@/auth/components/auth-guard"
export default function DashboardPage() {
return (
<AuthGuard>
<div>Your protected content here</div>
</AuthGuard>
)
}// app/login/page.tsx
import { WalletConnector } from "@/auth/components/wallet-connector"
export default function LoginPage() {
const handleConnect = (address: string) => {
console.log("Connected:", address)
// Handle wallet connection
}
return (
<WalletConnector
onConnect={handleConnect}
onDisconnect={() => console.log("Disconnected")}
/>
)
}import { useAuth, logout } from "@/auth/lib/auth-utils"
function MyComponent() {
const { isAuthenticated, address, isLoading } = useAuth()
if (isLoading) return <div>Loading...</div>
return (
<div>
{isAuthenticated ? (
<div>
<p>Connected: {address}</p>
<button onClick={logout}>Logout</button>
</div>
) : (
<p>Not authenticated</p>
)}
</div>
)
}// app/api/protected-endpoint/route.ts
import { verifyAuthSession } from "@/auth/lib/auth"
export async function GET() {
const session = await verifyAuthSession()
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
return Response.json({
message: "Access granted",
address: session.address
})
}Required environment variables:
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
# Optional
NODE_ENV=development # or productionThe authentication system requires a whitelist table in Supabase:
CREATE TABLE whitelist (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
bitcoin_address TEXT NOT NULL UNIQUE,
label TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_active BOOLEAN DEFAULT true
);See scripts/002_create_whitelist_table.sql for the complete schema setup.
INSERT INTO whitelist (bitcoin_address, label) VALUES
('bc1qyour_address_here', 'User Description');-- Soft delete (recommended)
UPDATE whitelist
SET is_active = false
WHERE bitcoin_address = 'bc1q...';
-- Hard delete
DELETE FROM whitelist
WHERE bitcoin_address = 'bc1q...';SELECT bitcoin_address, label, created_at, is_active
FROM whitelist
WHERE is_active = true
ORDER BY created_at DESC;✅ Unauthorized access (only whitelisted addresses can authenticate)
✅ Session hijacking (HttpOnly cookies prevent client-side access)
✅ Replay attacks (unique challenge messages with timestamps)
✅ Client-side bypass (all verification happens server-side)
✅ Database tampering (Row Level Security policies)
Edit lib/auth.ts:
export async function createAuthSession(address: string): Promise<string> {
const session: AuthSession = {
address,
isAuthenticated: true,
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // Change this value
}
// ...
}Edit middleware.ts:
const PUBLIC_ROUTES = ["/login", "/api/auth", "/public-page"]Edit lib/auth.ts:
export function generateChallengeMessage(): string {
const timestamp = Date.now()
const nonce = Math.random().toString(36).substring(2, 15)
return `Your Custom Message\nTimestamp: ${timestamp}\nNonce: ${nonce}`
}- Check if address is in whitelist table
- Verify
is_active = truefor the address - Check for typos in the Bitcoin address
- Ensure wallet is properly connected
- Try signing the message again
- Verify wallet compatibility (UniSat works best)
- Sessions expire after 24 hours
- Re-authenticate by visiting /login
- Check server time synchronization
- Verify public routes are properly excluded in middleware.ts
- Check that /login is in the PUBLIC_ROUTES array
- Clear browser cookies and cache
- Install the wallet extension (UniSat, Xverse, or OKX)
- Refresh the browser after installation
- Check browser console for connection errors
- Update whitelist with real authorized addresses
- Configure all environment variables in production
- Enable HTTPS for secure cookie transmission
- Review and test Row Level Security policies
- Consider shorter session timeouts for high-security needs
- Set up monitoring and logging
- Test with all supported wallet types
- Document authorized users and their addresses
Required npm packages:
{
"@supabase/ssr": "^0.x.x",
"@supabase/supabase-js": "^2.x.x",
"next": "^14.x.x",
"react": "^18.x.x"
}This authentication system is designed to be integrated into your Next.js application. Modify and use as needed.
For issues or questions:
- Check the troubleshooting section above
- Review the SETUP.md file for installation steps
- Verify your environment variables are correctly configured
- Check the Supabase dashboard for database connectivity
- Initial release
- Bitcoin wallet authentication
- Multi-wallet support (UniSat, Xverse, OKX)
- Supabase integration
- Route protection middleware
- Session management