This guide provides a complete step-by-step process to verify that Issue #45 (Stellar Wallet Authentication) has been successfully implemented.
Total Tests: 8 scenarios covering all acceptance criteria Estimated Time: 30-45 minutes
Before starting tests, ensure:
- Node.js installed (
v18+recommended) - Stellar wallet installed (Freighter or web access to Albedo)
- Connected to Stellar testnet
- Environment variables configured (
.env.local) - Database migrations applied
- Development server running (
npm run dev)
Step 1: Environment Variables
# Copy template
cp .env.example .env.local
# Edit .env.local and add:
# 1. DATABASE_URL - your NeonDB connection
# 2. JWT_SECRET - run: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# 3. NEXT_PUBLIC_STELLAR_NETWORK="testnet"Step 2: Database Migration
# Connect to your NeonDB and execute: scripts/add-auth-tables.sql
# This creates: users, auth_sessions, login_nonces tablesStep 3: Install Dependencies (if needed)
npm installStep 4: Start Dev Server
npm run dev
# Should show: ▲ Next.js X.X.X (ready - started server on 0.0.0.0:3000)Step 5: Verify Server is Running
- Navigate to http://localhost:3000
- Should see CodeCodely landing page
- No console errors
Objective: Verify wallet can connect and retrieve public key
Steps:
- Open http://localhost:3000 in browser
- Click "Connect Wallet" button in navbar
- Choose your wallet:
- For Freighter: Click "🚀 Freighter"
- For Albedo: Click "⭐ Albedo"
- Approve wallet connection request
- Verify public key is retrieved
Expected Results:
- Dialog shows wallet options
- Wallet requests approval (extension popup or new tab)
- Button changes to show shortened address (e.g., "GXXX...XXXX")
- No errors in browser console
- Wallet state shows "Connected"
Verification Commands:
// Open browser console (F12 → Console tab)
// Check localStorage
localStorage.getItem("authToken"); // Should return JWT
localStorage.getItem("walletAddress"); // Should return Stellar address
localStorage.getItem("walletName"); // Should return "Freighter" or "Albedo"
// Check context (if using React DevTools)
// Components → ClientWalletProvider → props → value✅ Pass Criteria:
- Wallet address appears in navbar
- LocalStorage contains authToken, walletAddress, walletName
- No console errors
Objective: Verify that signature verification works server-side
Steps:
- Start with disconnected wallet
- Open browser DevTools → Network tab → XHR filter
- Click "Connect Wallet" and select Freighter/Albedo
- Complete wallet connection
- Watch network requests
Expected Network Requests (in order):
Request 1: GET /api/auth/nonce
{
"nonce": "a1b2c3d4e5f6...",
"message": "Sign this nonce to login to Codely: a1b2c3d4e5f6..."
}Request 2: POST /api/auth/verify
Request Body:
{
"publicKey": "GXXXXXXX...",
"signature": "xxxxx...",
"nonce": "a1b2c3d4e5f6..."
}
Response (Status 200):
{
"token": "eyJhbGc...",
"user": {
"walletAddress": "GXXXXXXX...",
"createdAt": "2024-04-25T..."
},
"message": "Authentication successful"
}✅ Pass Criteria:
- Both requests complete successfully
- /api/auth/nonce returns valid nonce
- /api/auth/verify returns JWT token
- Status codes are 200
- JWT token is valid format (xxx.xxx.xxx)
Objective: Verify JWT token is properly generated and stored
Steps:
- After connecting wallet (Test 1), get the JWT token
- Decode it to verify payload
How to Check:
// In browser console:
const token = localStorage.getItem("authToken");
// Split token into parts
const parts = token.split(".");
const payload = JSON.parse(atob(parts[1]));
console.log("Payload:", payload);
console.log("Expiration:", new Date(payload.exp * 1000));
console.log("Wallet Address:", payload.walletAddress);Expected Payload:
{
"alg": "HS256",
"typ": "JWT",
"sub": "GXXXXXXX...",
"iat": 1719331234,
"exp": 1719936034,
"walletAddress": "GXXXXXXX..."
}✅ Pass Criteria:
- Token has 3 parts (header.payload.signature)
- Payload contains walletAddress
- Expiration is ~7 days in future
- No decoding errors
Objective: Verify nonce is single-use (cannot be replayed)
Steps:
Part A: Manual API Test
# 1. Get a nonce
curl http://localhost:3000/api/auth/nonce
# Copy the nonce from response
# 2. Create a test signature (use existing wallet connection)
# In browser console, get a signature:
const message = "Sign this nonce to login to Codely: YOUR_NONCE";
// (Have your wallet sign this, or use existing signature for testing)
# 3. First verification attempt (should work):
curl -X POST http://localhost:3000/api/auth/verify \
-H "Content-Type: application/json" \
-d '{
"publicKey": "YOUR_WALLET_ADDRESS",
"signature": "SIGNATURE_FROM_WALLET",
"nonce": "YOUR_NONCE"
}'
# Should return: Status 200 with JWT token
# 4. Second attempt with SAME nonce (should fail):
curl -X POST http://localhost:3000/api/auth/verify \
-H "Content-Type: application/json" \
-d '{
"publicKey": "YOUR_WALLET_ADDRESS",
"signature": "SIGNATURE_FROM_WALLET",
"nonce": "YOUR_NONCE"
}'
# Should return: Status 401 with "Invalid or expired nonce"✅ Pass Criteria:
- First attempt succeeds (Status 200)
- Second attempt fails (Status 401)
- Error message mentions "Invalid or expired nonce"
- Replay attack is prevented
Objective: Verify POST /api/snippets requires authentication
Steps:
Part A: Unauthenticated Request (Should Fail)
curl -X POST http://localhost:3000/api/snippets \
-H "Content-Type: application/json" \
-d '{
"title": "Test Snippet",
"description": "This should fail",
"code": "console.log(\"test\");",
"language": "javascript",
"tags": ["test"]
}'Expected Response (Status 401):
{
"error": "Unauthorized - Please authenticate with your wallet"
}Part B: Authenticated Request (Should Succeed)
# 1. Get token from browser
TOKEN=$(localStorage.getItem('authToken'))
# 2. Create snippet WITH authentication
curl -X POST http://localhost:3000/api/snippets \
-H "Content-Type: application/json" \
-H "Authorization: Bearer TOKEN_HERE" \
-d '{
"title": "My First Authenticated Snippet",
"description": "Created with wallet authentication",
"code": "console.log(\"Hello from authenticated user\");",
"language": "javascript",
"tags": ["authenticated", "test"]
}'Expected Response (Status 201):
{
"id": "uuid-here",
"title": "My First Authenticated Snippet",
"owner": "GXXXXXXX...",
"code": "console.log(\"Hello from authenticated user\");",
"language": "javascript",
"tags": ["authenticated", "test"],
"created_at": "2024-04-25T...",
"updated_at": "2024-04-25T..."
}✅ Pass Criteria:
- Without token: Status 401 with error message
- With token: Status 201 with snippet created
- Snippet has "owner" field matching wallet address
- No errors in console
Objective: Verify wallet session persists across page refresh
Steps:
- Connect wallet (should see address in navbar)
- Open DevTools → Application → LocalStorage → http://localhost:3000
- Note the values: authToken, walletAddress, walletName
- Refresh page (F5 or Cmd+R)
- Verify wallet remains connected
Expected Results:
- Before refresh: Address shown in navbar
- LocalStorage values remain unchanged
- After refresh: Address still shows (no need to reconnect)
- No console errors
✅ Pass Criteria:
- Wallet state restored after refresh
- No re-authentication needed
- LocalStorage persists correctly
Objective: Verify disconnect works and clears session
Steps:
- Connect wallet (address shows in navbar)
- Click on the wallet address in navbar
- Confirm disconnect
- Check browser console and LocalStorage
Expected Results:
- Button changes back to "Connect Wallet"
- LocalStorage values cleared:
localStorage.getItem("authToken"); // null localStorage.getItem("walletAddress"); // null localStorage.getItem("walletName"); // null
- Network shows POST /api/auth/logout succeeded
- No console errors
Verification:
// In browser console after logout:
localStorage.getItem("authToken"); // Should be null✅ Pass Criteria:
- All localStorage values cleared
- Button returns to disconnected state
- No errors during logout
Objective: Verify invalid signatures are rejected
Steps:
- Get a nonce
- Attempt verification with invalid/modified signature
- Observe error handling
Manual Test:
# Get nonce
NONCE=$(curl -s http://localhost:3000/api/auth/nonce | jq -r '.nonce')
# Try with invalid signature (modify/corrupt it)
curl -X POST http://localhost:3000/api/auth/verify \
-H "Content-Type: application/json" \
-d "{
\"publicKey\": \"GXXXXXXX...\",
\"signature\": \"invalid_signature_here\",
\"nonce\": \"$NONCE\"
}"Expected Response (Status 401):
{
"error": "Invalid signature"
}✅ Pass Criteria:
- Status code: 401
- Error message: "Invalid signature"
- No JWT token returned
- Server handled gracefully (no crash)
After completing all 8 tests, verify:
- Connect Wallet button visible
- Wallet selection dialog works
- Address displays when connected
- Disconnect works
- Session persists on refresh
- Error messages display properly
- Nonce generation endpoint works (GET /api/auth/nonce)
- Signature verification works (POST /api/auth/verify)
- JWT tokens are valid
- Replay protection works
- Protected routes require auth
- Logout invalidates sessions
-- Connect to your database and run:
SELECT * FROM users; -- Should have your wallet address
SELECT * FROM auth_sessions; -- Should have active session
SELECT * FROM login_nonces; -- Should have used nonce (used=true)
SELECT * FROM snippets WHERE owner IS NOT NULL; -- Should have created snippet- Private keys never transmitted to server
- Only public keys stored
- Tokens properly hashed in database
- Nonces are single-use
- Expiration times are enforced
- Clear error messages without leaking details
Solution:
- Install Freighter: https://www.freighter.app/
- Refresh page
- Try again
Possible Causes:
- Message format changed
- Nonce expired (> 15 minutes)
- Wallet not properly installed
Solution:
- Check browser console for exact message
- Get a new nonce
- Try within 15 minutes
- Check wallet is properly connected
Solution:
- Check Database connection (DATABASE_URL in .env.local)
- Verify SQL tables exist (run migration script)
- Check JWT_SECRET is set
Solution:
- Verify you're connected (wallet address in navbar)
- Check token exists:
localStorage.getItem('authToken') - Verify token format in network inspector
- Re-connect wallet to get fresh token
Solution:
// Add to next.config.mjs if needed:
headers: [
{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Origin", value: "http://localhost:3000" },
],
},
];✅ All 8 tests pass ✅ No console errors ✅ All expected data structures match ✅ Authentication flow complete ✅ Security measures verified
- Test 1: Basic Wallet Connection ✅
- Test 2: Signature Verification Flow ✅
- Test 3: JWT Token Validation ✅
- Test 4: Replay Attack Prevention ✅
- Test 5: Protected API Endpoint ✅
- Test 6: Session Persistence ✅
- Test 7: Logout Functionality ✅
- Test 8: Invalid Signature Handling ✅
When all tests pass, Issue #45 is successfully completed!
Implementation Date: [Date] Tested By: [Your Name] Status: ✅ COMPLETE