Skip to content

Commit 1475f8a

Browse files
Merge pull request #94 from frankosakwe/feature/wallet-signature-verification
feat: implement secure wallet signature verification flow with Freigh…
2 parents 81c0dc5 + 5127995 commit 1475f8a

2 files changed

Lines changed: 77 additions & 10 deletions

File tree

app/api/auth/verify/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
8484
const response = NextResponse.json(
8585
{
8686
walletAddress: normalizedWallet,
87+
accessToken: session.accessToken,
8788
accessTokenExpiresAt: session.accessTokenExpiresAt.toISOString(),
8889
refreshTokenExpiresAt: session.refreshTokenExpiresAt.toISOString(),
8990
},

app/login/page.tsx

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import React, { useState, useEffect } from 'react'
44
import { useRouter } from 'next/navigation'
5-
import { isConnected, requestAccess, getAddress } from '@stellar/freighter-api'
5+
import { isConnected, requestAccess, getAddress, signMessage } from '@stellar/freighter-api'
66
import { Button } from '@/components/ui/button'
77
import { Wallet, LogOut, AlertCircle, Loader2, ArrowLeft, FlaskConical } from 'lucide-react'
88
import Link from 'next/link'
@@ -14,6 +14,7 @@ export default function LoginPage() {
1414
const [address, setAddress] = useState<string | null>(null)
1515
const [isInitializing, setIsInitializing] = useState(true)
1616
const [isConnecting, setIsConnecting] = useState(false)
17+
const [statusMessage, setStatusMessage] = useState<string | null>(null)
1718
const [isMocking, setIsMocking] = useState(false)
1819
const [error, setError] = useState<string | null>(null)
1920

@@ -26,10 +27,17 @@ export default function LoginPage() {
2627
// Verify if still connected
2728
const connectedResponse = await isConnected()
2829
if (connectedResponse.isConnected) {
29-
setAddress(savedAddress)
30-
} else {
31-
localStorage.removeItem('stellar_wallet_address')
30+
// Also check if backend session is still active
31+
const meRes = await fetch('/api/auth/me')
32+
if (meRes.ok) {
33+
setAddress(savedAddress)
34+
router.push('/dashboard')
35+
return
36+
}
3237
}
38+
// Clear if not fully verified
39+
localStorage.removeItem('stellar_wallet_address')
40+
localStorage.removeItem('tc_dev_access_token')
3341
}
3442
} catch (err) {
3543
console.error('Failed to initialize wallet session:', err)
@@ -40,11 +48,12 @@ export default function LoginPage() {
4048

4149
// Slight delay to ensure Freighter extension is loaded
4250
setTimeout(initSession, 500)
43-
}, [])
51+
}, [router])
4452

4553
const handleConnect = async () => {
4654
setError(null)
4755
setIsConnecting(true)
56+
setStatusMessage('Connecting wallet...')
4857
try {
4958
// Check if Freighter is installed
5059
const connectedResponse = await isConnected()
@@ -69,17 +78,73 @@ export default function LoginPage() {
6978
walletAddress = addressResponse.address
7079
}
7180

72-
if (walletAddress) {
73-
setAddress(walletAddress)
74-
localStorage.setItem('stellar_wallet_address', walletAddress)
75-
} else {
81+
if (!walletAddress) {
7682
throw new Error('Failed to retrieve wallet address from Freighter.')
7783
}
84+
85+
// 1. Fetch Nonce & Message
86+
setStatusMessage('Requesting authentication message...')
87+
const nonceRes = await fetch('/api/auth/nonce', {
88+
method: 'POST',
89+
headers: { 'Content-Type': 'application/json' },
90+
body: JSON.stringify({ walletAddress }),
91+
})
92+
const nonceData = await nonceRes.json()
93+
if (!nonceRes.ok) {
94+
throw new Error(nonceData?.error || 'Failed to generate authentication nonce.')
95+
}
96+
97+
const { nonce, message } = nonceData
98+
99+
// 2. Request signature via Freighter signMessage
100+
setStatusMessage('Approve signature request...')
101+
const signResponse = await signMessage(message)
102+
103+
let signature: string | undefined
104+
if (typeof signResponse === 'string') {
105+
signature = signResponse
106+
} else if (signResponse && typeof signResponse === 'object') {
107+
if (signResponse.error) {
108+
throw new Error(signResponse.error.message || signResponse.error || 'Message signing was rejected.')
109+
}
110+
signature = signResponse.signedMessage
111+
}
112+
113+
if (!signature) {
114+
throw new Error('Message signing was cancelled or failed.')
115+
}
116+
117+
// 3. Verify Signature & Nonce on Backend
118+
setStatusMessage('Verifying signature...')
119+
const verifyRes = await fetch('/api/auth/verify', {
120+
method: 'POST',
121+
headers: { 'Content-Type': 'application/json' },
122+
body: JSON.stringify({
123+
walletAddress,
124+
nonce,
125+
signature,
126+
message,
127+
}),
128+
})
129+
130+
const verifyData = await verifyRes.json()
131+
if (!verifyRes.ok) {
132+
throw new Error(verifyData?.error || 'Signature verification failed.')
133+
}
134+
135+
// 4. Set local storage & redirect
136+
setAddress(walletAddress)
137+
localStorage.setItem('stellar_wallet_address', walletAddress)
138+
if (verifyData.accessToken) {
139+
localStorage.setItem('tc_dev_access_token', verifyData.accessToken)
140+
}
141+
router.push('/dashboard')
78142
} catch (err: any) {
79143
console.error('Connection error:', err)
80144
setError(err?.message || 'Unable to connect wallet. Please try again.')
81145
} finally {
82146
setIsConnecting(false)
147+
setStatusMessage(null)
83148
}
84149
}
85150

@@ -108,6 +173,7 @@ export default function LoginPage() {
108173
const handleDisconnect = () => {
109174
setAddress(null)
110175
localStorage.removeItem('stellar_wallet_address')
176+
localStorage.removeItem('tc_dev_access_token')
111177
setError(null)
112178
}
113179

@@ -185,7 +251,7 @@ export default function LoginPage() {
185251
{isConnecting ? (
186252
<>
187253
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
188-
Connecting...
254+
{statusMessage || 'Connecting...'}
189255
</>
190256
) : (
191257
'Connect Freighter'

0 commit comments

Comments
 (0)