# Phase 3: Clerk Authentication Integration
## Overview
Phase 3 completes the wallet integration by connecting it to Clerk authentication, implementing middleware protection, and creating a unified authentication flow. This phase eliminates the need for separate auth systems and provides a seamless experience.
## Timeline
**Duration**: 2 weeks
**Dependencies**: Phase 1 & 2 completion
**Team**: Auth & Backend team
## Architecture
```mermaid
graph TD
A[Browser] --> B[Next.js Middleware]
B --> C{Auth Check}
C -->|No Auth| D[Clerk Sign-in]
C -->|Has Auth| E[Protected Routes]
D --> F[GitHub OAuth]
D --> G[Wallet Auth]
F --> H[Clerk Session]
G --> H
H --> I[User Metadata]
I --> J[Wallet Address]
E --> K[Wallet Operations]
K --> L[API Routes]
L --> M[Server Actions]
```
## Migration Reference
These implementations adapt functionality from:
- Wallet auth flow → from `/repos/laconic-wallet-web/src/screens/AutoSignIn.tsx`
- Session management → from `/repos/laconic-wallet-web/src/App.tsx` session handling
- Clerk integration → extends existing `/apps/deploy-fe/src/middleware.ts`
## Security Considerations
This phase deals with authentication and sensitive wallet operations. Consider these critical security aspects:
1. **Session Management**: Implement proper session handling using Clerk's secure mechanisms
2. **Wallet-Clerk Linking**: Verify wallet ownership through cryptographic signatures before linking to a user account
3. **Authorization**: Use proper authorization checks for all wallet operations
4. **Error States**: Gracefully handle network failures and blockchain errors
5. **Rate Limiting**: Implement rate limiting for sensitive operations like wallet linking
## Step-by-Step Implementation
### 1. Extend Clerk User Metadata
Create a TypeScript interface for the extended user metadata in `apps/deploy-fe/src/types/clerk.d.ts`:
```typescript
import { User } from '@clerk/nextjs/server'
declare module '@clerk/nextjs/server' {
interface User {
publicMetadata: {
walletAddress?: string
walletChainId?: string
walletConnected?: boolean
}
}
}
```
### 2. Implement Clerk API Routes for Wallet Auth
Create `apps/deploy-fe/src/app/api/clerk/wallet/link/route.ts`:
```typescript
import { auth, clerkClient } from '@clerk/nextjs/server'
import { validateSignature } from '@workspace/wallet-core'
import { NextResponse } from 'next/server'
import { z } from 'zod'
// Define request validation schema with Zod
const linkWalletSchema = z.object({
address: z.string()
.refine(addr => /^(0x[a-fA-F0-9]{40}|laconic[a-zA-Z0-9]{39,59})$/.test(addr), {
message: "Invalid wallet address format"
}),
message: z.string().min(10),
signature: z.string().min(1),
chainId: z.string().min(1)
})
export async function POST(request: Request) {
// Rate limiting - in production implement proper rate limiting middleware
// This is a placeholder to show where it should be implemented
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const body = await request.json()
// Validate request data
const validationResult = linkWalletSchema.safeParse(body)
if (!validationResult.success) {
return NextResponse.json(
{ error: 'Invalid request data', details: validationResult.error.format() },
{ status: 400 }
)
}
const { address, message, signature, chainId } = validationResult.data
// Ensure the signed message actually contains the correct address
// This prevents replay attacks where a valid signature for one address is used for another
if (!message.includes(address)) {
return NextResponse.json(
{ error: 'Address in message does not match provided address' },
{ status: 400 }
)
}
// Validate signature using wallet-core
const isValid = await validateSignature(message, signature)
if (!isValid.success) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 400 }
)
}
// Update Clerk user metadata with wallet info
await clerkClient.users.updateUser(userId, {
publicMetadata: {
walletAddress: address,
walletChainId: chainId,
walletConnected: true
}
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error linking wallet:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
)
}
}
```
Create `apps/deploy-fe/src/app/api/clerk/wallet/unlink/route.ts`:
```typescript
import { auth, clerkClient } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const { userId } = await auth()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const user = await clerkClient.users.getUser(userId)
// Remove wallet information from metadata
await clerkClient.users.updateUser(userId, {
publicMetadata: {
...user.publicMetadata,
walletAddress: null,
walletChainId: null,
walletConnected: false
}
})
return NextResponse.json({ success: true })
} catch (error) {
console.error('Error unlinking wallet:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
)
}
}
```
### 3. Enhance Middleware with Wallet Verification
Update `apps/deploy-fe/src/middleware.ts`:
```typescript
import { clerkMiddleware, createRouteMatcher, getAuth } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
// Define routes that require wallet connection
const requiresWalletAuth = createRouteMatcher([
'/wallet(.*)',
'/projects(.*)',
'/buy-prepaid-service(.*)'
])
// Define public routes that don't require any auth
const isPublicRoute = createRouteMatcher([
'/sign-in(.*)',
'/sign-up(.*)',
'/api/github/webhook'
])
export default clerkMiddleware(async (auth, req) => {
const { userId } = auth
// Skip auth check for webhook endpoint
if (req.nextUrl.pathname === '/api/github/webhook') {
return NextResponse.next()
}
// For public routes, allow access
if (isPublicRoute(req)) {
return NextResponse.next()
}
// For all other routes, require authentication
if (!userId) {
return NextResponse.redirect(new URL('/sign-in', req.url))
}
// For wallet-required routes, check wallet connection
if (requiresWalletAuth(req)) {
const user = auth.user
// If wallet not connected, redirect to wallet connection page
if (!user?.publicMetadata?.walletConnected) {
return NextResponse.redirect(new URL('/wallet/connect', req.url))
}
}
return NextResponse.next()
})
export const config = {
matcher: [
// Skip Next.js internals and all static files
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)'
]
}
```
### 4. Create Wallet Connection Page
Create `apps/deploy-fe/src/app/(web3-authenticated)/wallet/connect/page.tsx`:
```tsx
'use client'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useUser } from '@clerk/nextjs'
import { WalletConnectButton, useWalletUI } from '@workspace/ui/wallet'
import { Button } from '@workspace/ui/components/button'
import { PageWrapper } from '@/components/foundation'
import { createSiweMessage, signMessage } from '@workspace/wallet-core'
import { toast } from 'sonner'
import { ArrowLeft } from 'lucide-react'
export default function ConnectWalletPage() {
const router = useRouter()
const { isConnected, wallet } = useWalletUI()
const { user, isLoaded } = useUser()
const [isLinking, setIsLinking] = useState(false)
// Check if user already has a linked wallet
useEffect(() => {
if (isLoaded && user?.publicMetadata?.walletConnected) {
router.push('/')
}
}, [isLoaded, user, router])
// Function to link wallet to Clerk user
const linkWallet = async () => {
if (!wallet?.address) return
setIsLinking(true)
try {
// Create SIWE message
const message = await createSiweMessage(wallet.address)
// Sign message with wallet
const signature = await signMessage({
message,
namespace: 'eip155',
chainId: '1',
accountId: 0
})
// Link wallet to Clerk user
const response = await fetch('/api/clerk/wallet/link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
address: wallet.address,
message,
signature,
chainId: '1'
})
})
if (response.ok) {
toast.success('Wallet connected successfully')
router.push('/')
} else {
const data = await response.json()
throw new Error(data.error || 'Failed to link wallet')
}
} catch (error) {
console.error('Error linking wallet:', error)
toast.error(error instanceof Error ? error.message : 'Failed to link wallet')
} finally {
setIsLinking(false)
}
}
return (
Connect your wallet to access additional features like deployments
and blockchain-related functionality.
Wallet connected: {wallet?.address?.slice(0, 6)}...{wallet?.address?.slice(-4)}
Connect Your Wallet
Connected Address:
{walletAddress.slice(0, 8)}...{walletAddress.slice(-6)}
No wallet connected to your account