This guide explains the implementation of Issue #45: Stellar Wallet Authentication (Full Login Flow) for CodeCodely.
1. User clicks "Connect Wallet"
2. Frontend requests nonce from /api/auth/nonce
3. Wallet signs nonce with user's private key
4. Frontend sends signature to /api/auth/verify
5. Backend verifies signature against public key
6. Backend issues JWT token
7. Frontend stores JWT in localStorage
8. All subsequent API requests include JWT in Authorization header
9. Middleware validates JWT on protected routes
- users - Stores wallet addresses of authenticated users
- auth_sessions - Stores JWT session tokens with expiration
- login_nonces - Stores one-time nonces for signature verification (prevents replay attacks)
generateJWT()- Creates JWT tokens for wallet addressesverifyJWT()- Validates JWT tokensgenerateNonce()- Creates one-time signature noncesverifyNonce()- Validates nonce (prevents replay attacks)verifyWalletSignature()- Validates Stellar wallet signaturesgetOrCreateUser()- Manages user records
POST /api/auth/nonce- Generate authentication noncePOST /api/auth/verify- Verify signature and issue JWTPOST /api/auth/logout- Invalidate session
verifyAuthentication()- Extract and verify JWT from requestswithAuth()- HOF to protect API routesauthMiddleware()- Middleware for route protection
- Signature-based login flow
- JWT token storage in localStorage
- Automatic reconnection on page load
- Logout functionality
- Error handling
/api/snippets- Now requires authentication- Snippets linked to wallet owner
Run the SQL migration to add authentication tables:
# This will be run against your NeonDB database
# The migration is in: scripts/add-auth-tables.sqlExecute the migration:
-- Connect to your NeonDB database and run:
-- File: scripts/add-auth-tables.sqlCopy .env.example to .env.local and configure:
# Required
DATABASE_URL="postgresql://user:password@host/database?sslmode=require"
JWT_SECRET="<generate-secure-random-key>"
NEXT_PUBLIC_STELLAR_NETWORK="testnet"Generate a secure JWT_SECRET:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"The project already has the required dependencies:
@creit-tech/stellar-wallets-kit- For Stellar operations@albedo-link/intent- For Albedo walletzod- For validation
If using with a different Stellar library, ensure stellar-sdk is installed:
npm install stellar-sdkEnsure your wallet has the signMessage method available:
Freighter:
const message = "Your message";
const signature = await window.freighter.signMessage(message, { domain: 'codely.app' });Albedo:
const albedo = require('@albedo-link/intent').default;
const result = await albedo.signMessage({ message });npm run devVisit http://localhost:3000 to test the wallet authentication.
Prerequisites:
- Have a Stellar wallet installed (Freighter or use Albedo web wallet)
- Connected to testnet
Steps:
- Open http://localhost:3000
- Click "Connect Wallet"
- Select "Freighter" or "Albedo"
- Approve the connection in your wallet
- When prompted, sign the nonce message with your wallet
Expected Results:
- Wallet connects successfully
- You see a shortened wallet address in the navbar (e.g.,
GXXX...XXXX) - No errors in console
- JWT token stored in localStorage
Verification:
// In browser console:
localStorage.getItem('authToken') // Should show JWT token
localStorage.getItem('walletAddress') // Should show Stellar addressSteps:
- Open browser DevTools → Network tab
- Connect wallet as in Test 1
- Look at the
/api/auth/verifyPOST request
Expected Results:
- Request includes
publicKey,signature,nonce - Response includes JWT token
- Status code: 200
Example Response:
{
"token": "eyJhbGc...",
"user": {
"walletAddress": "GXXXXXXX...",
"createdAt": "2024-04-25T..."
},
"message": "Authentication successful"
}Steps:
- Connect wallet and get a nonce (via /api/auth/nonce)
- Sign the nonce and verify it once (POST /api/auth/verify)
- Try to use the SAME nonce and signature again
Expected Results:
- First authentication: Success (Status 200)
- Second attempt with same nonce: Fails (Status 401)
- Error message: "Invalid or expired nonce"
Manual Test:
# Get nonce
curl http://localhost:3000/api/auth/nonce
# Verify with nonce (first time - works)
curl -X POST http://localhost:3000/api/auth/verify \
-H "Content-Type: application/json" \
-d '{
"publicKey": "YOUR_WALLET_ADDRESS",
"signature": "SIGNATURE",
"nonce": "NONCE"
}'
# Try again with same nonce (fails)
curl -X POST http://localhost:3000/api/auth/verify \
-H "Content-Type: application/json" \
-d '{
"publicKey": "YOUR_WALLET_ADDRESS",
"signature": "SIGNATURE",
"nonce": "NONCE"
}'Prerequisites:
- Wallet connected (JWT token in localStorage)
Steps:
- Connect wallet to get JWT token
- Create a snippet via POST /api/snippets
- Include JWT in Authorization header
Expected Results:
- Snippet created with owner = wallet address
- Status code: 201
Manual Test:
# Get token from browser console
TOKEN=$(localStorage.getItem('authToken'))
# Create snippet with JWT
curl -X POST http://localhost:3000/api/snippets \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"title": "Hello World",
"description": "Test snippet",
"code": "console.log(\"hello\");",
"language": "javascript",
"tags": ["test"]
}'Steps:
- Without connecting wallet, try to create a snippet
- Send POST request without JWT token
Expected Results:
- Status code: 401
- Error message: "Unauthorized - Please authenticate with your wallet"
Manual Test:
curl -X POST http://localhost:3000/api/snippets \
-H "Content-Type: application/json" \
-d '{
"title": "Test",
"description": "Test",
"code": "test",
"language": "javascript",
"tags": ["test"]
}'
# Returns 401 UnauthorizedSteps:
- Connect wallet
- Refresh the page (F5)
- Check if wallet remains connected
Expected Results:
- Wallet address still visible after refresh
- No need to re-authenticate
- JWT token restored from localStorage
Steps:
- Connect wallet
- Click on wallet address in navbar to disconnect
- Check localStorage
Expected Results:
- Wallet disconnects
- JWT token removed from localStorage
- "Connect Wallet" button appears again
- Logout request sent to /api/auth/logout
Steps:
- Get a valid nonce
- Manually modify the signature (change one character)
- Send to /api/auth/verify with modified signature
Expected Results:
- Status code: 401
- Error message: "Invalid signature"
Solution:
- Install Freighter extension: https://www.freighter.app/
- Refresh the page
- Try again
Possible causes:
- Wallet not signing the exact nonce message
- Signature verification library issue
- Message encoding mismatch
Solution:
- Check console logs for exact message being signed
- Ensure nonce is in the correct format
Solution:
- Tokens expire after 7 days
- Reconnect wallet to get a new token
Solution:
- Verify DATABASE_URL is correct
- Check NeonDB connection settings
- Ensure SSL is required:
?sslmode=require
Solution:
- Nonces expire after 15 minutes
- Get a new nonce via
/api/auth/nonce - Complete authentication within 15 minutes
- JWT_SECRET is set to a strong random value (minimum 32 characters)
- Database URL uses SSL connection
- Private keys are NEVER stored on server
- Only public keys are used for signature verification
- Nonces are single-use (replay protection)
- JWT tokens have expiration time
- Sensitive errors don't leak implementation details
- Rate limiting implemented on auth endpoints (recommended)
- CORS configured for frontend domain
- Implement refresh token rotation
- Add rate limiting to auth endpoints
- Implement multi-signature support
- Add user profile/metadata storage
- Implement snippet permission system
- Add session management UI
- Implement wallet connection revocation
- Change JWT_SECRET to production value
- Set NEXT_PUBLIC_STELLAR_NETWORK to "public"
- Use secure database with proper backups
- Implement request rate limiting
- Add monitoring and alerting
- Consider audit logging for authentication events
- Implement DDoS protection