Web3 Nonce Generation
Overviewβ
The /api/auth/crypto/generateNonce
endpoint generates cryptographic nonces for Web3 wallet authentication. This endpoint is essential for the MetaMask/Web3 login flow, providing secure message signing verification.
Endpoint Detailsβ
POST /api/auth/crypto/generateNonce
Authentication: None required (public endpoint) Rate Limit: 10 requests per minute per IP
Requestβ
Headersβ
Content-Type: application/json
Accept: application/json
Body Parametersβ
Parameter | Type | Required | Description |
---|---|---|---|
address | string | Yes | Ethereum wallet address (0x...) |
Request Exampleβ
{
"address": "0x742d35Cc6634C0532925a3b8D4C2C4e0C8A8C8C8"
}
Responseβ
Success Response (200)β
{
"nonce": "Sign this message to authenticate with Ring Platform: 1703123456789",
"timestamp": 1703123456789,
"expiresAt": 1703123756789
}
Response Fieldsβ
Field | Type | Description |
---|---|---|
nonce | string | Formatted message for wallet signing |
timestamp | number | Unix timestamp when nonce was generated |
expiresAt | number | Unix timestamp when nonce expires (5 minutes) |
Authentication Flowβ
Complete Web3 Authentication Processβ
Code Examplesβ
Frontend Integration (React/TypeScript)β
import { ethers } from 'ethers'
import { signIn } from 'next-auth/react'
async function authenticateWithWallet() {
try {
// 1. Request account access
if (!window.ethereum) {
throw new Error('MetaMask not installed')
}
const provider = new ethers.BrowserProvider(window.ethereum)
const accounts = await provider.send('eth_requestAccounts', [])
const address = accounts[0]
// 2. Generate nonce
const nonceResponse = await fetch('/api/auth/crypto/generateNonce', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address })
})
const { nonce } = await nonceResponse.json()
// 3. Sign message
const signer = await provider.getSigner()
const signature = await signer.signMessage(nonce)
// 4. Authenticate with NextAuth
const result = await signIn('credentials', {
address,
signature,
message: nonce,
redirect: false
})
if (result?.ok) {
console.log('Authentication successful')
} else {
console.error('Authentication failed:', result?.error)
}
} catch (error) {
console.error('Wallet authentication error:', error)
}
}
Backend Verification (NextAuth Credentials Provider)β
import { ethers } from 'ethers'
const credentialsProvider = CredentialsProvider({
id: 'web3',
name: 'Web3 Wallet',
credentials: {
address: { label: 'Address', type: 'text' },
signature: { label: 'Signature', type: 'text' },
message: { label: 'Message', type: 'text' }
},
async authorize(credentials) {
try {
if (!credentials?.address || !credentials?.signature || !credentials?.message) {
return null
}
// Verify signature
const recoveredAddress = ethers.verifyMessage(
credentials.message,
credentials.signature
)
if (recoveredAddress.toLowerCase() !== credentials.address.toLowerCase()) {
return null
}
// Check nonce validity (implement your nonce storage/validation)
const isValidNonce = await validateNonce(credentials.message)
if (!isValidNonce) {
return null
}
// Get or create user
const user = await getUserByWalletAddress(credentials.address)
return {
id: user.id,
name: user.name || `User ${credentials.address.slice(0, 6)}...`,
email: user.email || `${credentials.address}@wallet.local`,
address: credentials.address
}
} catch (error) {
console.error('Web3 auth error:', error)
return null
}
}
})
cURL Exampleβ
# Generate nonce
curl -X POST "https://ring.ck.ua/api/auth/crypto/generateNonce" \
-H "Content-Type: application/json" \
-d '{
"address": "0x742d35Cc6634C0532925a3b8D4C2C4e0C8A8C8C8"
}'
Python Exampleβ
import requests
import json
from eth_account import Account
from eth_account.messages import encode_defunct
def authenticate_with_wallet(private_key, address):
# Generate nonce
nonce_response = requests.post(
'https://ring.ck.ua/api/auth/crypto/generateNonce',
json={'address': address}
)
nonce_data = nonce_response.json()
# Sign message
message = encode_defunct(text=nonce_data['nonce'])
signed_message = Account.sign_message(message, private_key)
# Authenticate
auth_response = requests.post(
'https://ring.ck.ua/api/auth/callback/credentials',
data={
'address': address,
'signature': signed_message.signature.hex(),
'message': nonce_data['nonce']
}
)
return auth_response.json()
Error Handlingβ
Error Responsesβ
Invalid Address (400)β
{
"error": "INVALID_ADDRESS",
"message": "Invalid Ethereum address format",
"code": 400
}
Rate Limit Exceeded (429)β
{
"error": "RATE_LIMIT_EXCEEDED",
"message": "Too many nonce requests. Please try again later.",
"code": 429,
"retryAfter": 60
}
Server Error (500)β
{
"error": "INTERNAL_ERROR",
"message": "Failed to generate nonce",
"code": 500
}
Security Considerationsβ
Nonce Propertiesβ
- Uniqueness: Each nonce is unique per address and timestamp
- Expiration: Nonces expire after 5 minutes
- Single Use: Each nonce can only be used once
- Format: Includes timestamp and platform identifier
Message Formatβ
Sign this message to authenticate with Ring Platform: {timestamp}
Signature Verificationβ
- Address Recovery: Verify signature recovers to provided address
- Nonce Validation: Check nonce hasn't expired or been used
- Message Integrity: Ensure message matches generated nonce
- Replay Protection: Prevent signature reuse
Implementation Detailsβ
Nonce Storageβ
interface NonceRecord {
address: string
nonce: string
timestamp: number
expiresAt: number
used: boolean
}
// Store in Redis or database
const storeNonce = async (record: NonceRecord) => {
await redis.setex(
`nonce:${record.address}:${record.timestamp}`,
300, // 5 minutes TTL
JSON.stringify(record)
)
}
Validation Logicβ
const validateNonce = async (message: string, address: string): Promise<boolean> => {
const timestampMatch = message.match(/: (\d+)$/)
if (!timestampMatch) return false
const timestamp = parseInt(timestampMatch[1])
const now = Date.now()
// Check expiration (5 minutes)
if (now - timestamp > 300000) return false
// Check if nonce exists and hasn't been used
const nonceKey = `nonce:${address}:${timestamp}`
const nonceData = await redis.get(nonceKey)
if (!nonceData) return false
const nonce = JSON.parse(nonceData)
if (nonce.used) return false
// Mark as used
nonce.used = true
await redis.setex(nonceKey, 300, JSON.stringify(nonce))
return true
}
Testingβ
Unit Testsβ
describe('Nonce Generation', () => {
test('should generate valid nonce for valid address', async () => {
const response = await request(app)
.post('/api/auth/crypto/generateNonce')
.send({ address: '0x742d35Cc6634C0532925a3b8D4C2C4e0C8A8C8C8' })
.expect(200)
expect(response.body.nonce).toMatch(/^Sign this message to authenticate with Ring Platform: \d+$/)
expect(response.body.timestamp).toBeGreaterThan(Date.now() - 1000)
expect(response.body.expiresAt).toBe(response.body.timestamp + 300000)
})
test('should reject invalid address', async () => {
await request(app)
.post('/api/auth/crypto/generateNonce')
.send({ address: 'invalid-address' })
.expect(400)
})
})
Integration Testsβ
describe('Web3 Authentication Flow', () => {
test('should complete full authentication', async () => {
const wallet = ethers.Wallet.createRandom()
// Generate nonce
const nonceResponse = await request(app)
.post('/api/auth/crypto/generateNonce')
.send({ address: wallet.address })
const { nonce } = nonceResponse.body
// Sign message
const signature = await wallet.signMessage(nonce)
// Authenticate
const authResponse = await request(app)
.post('/api/auth/callback/credentials')
.send({
address: wallet.address,
signature,
message: nonce
})
expect(authResponse.status).toBe(200)
})
})
Related Endpointsβ
/api/auth/[...nextauth]
- Main authentication endpoint/api/profile
- User profile management/api/wallet/create
- Wallet creation
Troubleshootingβ
Common Issuesβ
Issue: "Invalid signature" Solution: Ensure message is signed exactly as provided by nonce endpoint
Issue: "Nonce expired" Solution: Generate new nonce, signatures must be completed within 5 minutes
Issue: "Address mismatch" Solution: Verify the signing address matches the address used to generate nonce
Issue: "MetaMask not detected" Solution: Ensure MetaMask extension is installed and enabled