Skip to main content

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​

ParameterTypeRequiredDescription
addressstringYesEthereum 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​

FieldTypeDescription
noncestringFormatted message for wallet signing
timestampnumberUnix timestamp when nonce was generated
expiresAtnumberUnix 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​

  1. Address Recovery: Verify signature recovers to provided address
  2. Nonce Validation: Check nonce hasn't expired or been used
  3. Message Integrity: Ensure message matches generated nonce
  4. 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)
})
})

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