Skip to main content

Overview

GenLayer Points uses Sign-In With Ethereum (SIWE) for decentralized authentication. Users sign in with their Ethereum wallet (MetaMask or compatible) instead of traditional passwords. Benefits:
  • No password management required
  • True decentralization
  • Wallet-based identity
  • Cryptographic proof of ownership
  • Better security and privacy

Authentication Flow

Implementation Details

Frontend Implementation

Location: frontend/src/lib/auth.js
1

Connect to wallet

import { ethers } from 'ethers';

async function connectWallet() {
  // Check if MetaMask is installed
  if (!window.ethereum) {
    throw new Error('Please install MetaMask');
  }
  
  // Request account access
  const provider = new ethers.BrowserProvider(window.ethereum);
  const accounts = await provider.send('eth_requestAccounts', []);
  const address = accounts[0];
  
  return { provider, address };
}
2

Get nonce from backend

import axios from 'axios';

async function getNonce(address) {
  const response = await axios.get(
    `${API_URL}/api/auth/nonce/`,
    { params: { address } }
  );
  return response.data.nonce;
}
3

Create and sign SIWE message

import { SiweMessage } from 'siwe';

async function signMessage(address, nonce, provider) {
  // Create SIWE message
  const domain = window.location.host;
  const origin = window.location.origin;
  
  const siweMessage = new SiweMessage({
    domain,
    address,
    statement: 'Sign in to GenLayer Points',
    uri: origin,
    version: '1',
    chainId: 1,  // Ethereum mainnet
    nonce
  });
  
  const message = siweMessage.prepareMessage();
  
  // Sign with MetaMask
  const signer = await provider.getSigner();
  const signature = await signer.signMessage(message);
  
  return { message, signature };
}
4

Send to backend for verification

async function login(address, message, signature) {
  const response = await axios.post(
    `${API_URL}/api/auth/login/`,
    { address, message, signature },
    { withCredentials: true }  // Important: include cookies
  );
  
  return response.data;
}
5

Complete authentication flow

export async function signInWithEthereum() {
  try {
    // 1. Connect wallet
    const { provider, address } = await connectWallet();
    
    // 2. Get nonce
    const nonce = await getNonce(address);
    
    // 3. Sign message
    const { message, signature } = await signMessage(address, nonce, provider);
    
    // 4. Login
    await login(address, message, signature);
    
    // 5. Update auth state
    authState.set({
      isAuthenticated: true,
      address
    });
    
    // 6. Load user data
    await userStore.loadUser();
    
    return true;
  } catch (error) {
    console.error('Authentication failed:', error);
    throw error;
  }
}

Backend Implementation

Location: backend/api/views.py and backend/ethereum_auth/authentication.py
1

Generate nonce endpoint

from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
import secrets

@csrf_exempt
def get_nonce(request):
    """Generate a random nonce for SIWE authentication."""
    address = request.GET.get('address')
    
    if not address:
        return JsonResponse({'error': 'Address required'}, status=400)
    
    # Generate cryptographically secure random nonce
    nonce = secrets.token_hex(16)
    
    # Store in session
    request.session['siwe_nonce'] = nonce
    request.session['siwe_address'] = address.lower()
    
    return JsonResponse({'nonce': nonce})
2

Verify signature endpoint

from siwe import SiweMessage
from eth_account.messages import encode_defunct
from web3 import Web3
from django.contrib.auth import get_user_model

User = get_user_model()

@csrf_exempt
def login(request):
    """Verify SIWE signature and create session."""
    if request.method != 'POST':
        return JsonResponse({'error': 'POST required'}, status=405)
    
    data = json.loads(request.body)
    address = data.get('address')
    message = data.get('message')
    signature = data.get('signature')
    
    # Validate required fields
    if not all([address, message, signature]):
        return JsonResponse({'error': 'Missing required fields'}, status=400)
    
    try:
        # Parse SIWE message
        siwe_message = SiweMessage.from_message(message)
        
        # Verify the message matches stored nonce
        stored_nonce = request.session.get('siwe_nonce')
        if not stored_nonce or siwe_message.nonce != stored_nonce:
            return JsonResponse({'error': 'Invalid nonce'}, status=400)
        
        # Verify signature
        siwe_message.verify(signature)
        
        # Verify address matches
        if siwe_message.address.lower() != address.lower():
            return JsonResponse({'error': 'Address mismatch'}, status=400)
        
        # Get or create user
        user, created = User.objects.get_or_create(
            address__iexact=address,
            defaults={
                'address': address.lower(),
                'email': f"{address.lower()}@ethereum.local",
                'username': address.lower()[:30]
            }
        )
        
        # Create session
        request.session['authenticated'] = True
        request.session['ethereum_address'] = address.lower()
        request.session['user_id'] = user.id
        
        # Clear nonce
        del request.session['siwe_nonce']
        
        return JsonResponse({
            'success': True,
            'user': {
                'id': user.id,
                'address': user.address,
                'name': user.name
            }
        })
        
    except Exception as e:
        logger.error(f"SIWE verification failed: {str(e)}")
        return JsonResponse({'error': 'Verification failed'}, status=400)
3

Authentication class

# ethereum_auth/authentication.py
from rest_framework import authentication
from django.contrib.auth import get_user_model

User = get_user_model()

class EthereumAuthentication(authentication.BaseAuthentication):
    """Authenticate requests using session-based Ethereum auth."""
    
    def authenticate(self, request):
        # Check session for authentication
        ethereum_address = request.session.get('ethereum_address')
        authenticated = request.session.get('authenticated', False)
        
        if not ethereum_address or not authenticated:
            return None
        
        try:
            # Get user by address
            user = User.objects.get(address__iexact=ethereum_address)
            return (user, None)
        except User.DoesNotExist:
            return None
4

Configure in settings

# backend/settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'ethereum_auth.authentication.EthereumAuthentication',
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
}

Session Management

# backend/settings.py

# Session configuration
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SESSION_COOKIE_SECURE = not DEBUG  # HTTPS only in production
SESSION_COOKIE_HTTPONLY = True  # Prevent XSS attacks
SESSION_COOKIE_SAMESITE = 'Lax'  # CSRF protection
SESSION_COOKIE_AGE = 1209600  # 2 weeks

# CORS configuration (must include credentials)
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
    'http://localhost:5173',  # Frontend dev
    'https://points.genlayer.com'  # Production
]

Frontend Axios Configuration

// frontend/src/lib/api.js
import axios from 'axios';

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true,  // CRITICAL: Include session cookie
  headers: {
    'Content-Type': 'application/json'
  }
});

export default apiClient;
Always set withCredentials: true in Axios to include session cookies in requests. Without this, authentication will fail.

Authentication State Management

Auth Store

Location: frontend/src/lib/auth.js
import { writable } from 'svelte/store';

function createAuthStore() {
  const { subscribe, set, update } = writable({
    isAuthenticated: false,
    address: null,
    loading: true
  });
  
  return {
    subscribe,
    
    // Connect wallet and sign in
    async signIn() {
      const { provider, address } = await connectWallet();
      const nonce = await getNonce(address);
      const { message, signature } = await signMessage(address, nonce, provider);
      await login(address, message, signature);
      
      set({ isAuthenticated: true, address, loading: false });
      await userStore.loadUser();
    },
    
    // Verify existing session
    async verify() {
      try {
        const response = await axios.get('/api/auth/verify/');
        if (response.data.authenticated) {
          set({
            isAuthenticated: true,
            address: response.data.address,
            loading: false
          });
          await userStore.loadUser();
        } else {
          set({ isAuthenticated: false, address: null, loading: false });
        }
      } catch (error) {
        set({ isAuthenticated: false, address: null, loading: false });
      }
    },
    
    // Logout
    async logout() {
      await axios.post('/api/auth/logout/');
      set({ isAuthenticated: false, address: null, loading: false });
      userStore.clearUser();
    }
  };
}

export const authState = createAuthStore();

Using Auth State in Components

// In a Svelte component
import { authState } from '../lib/auth.js';

let isLoading = $derived($authState.loading);
let isAuthenticated = $derived($authState.isAuthenticated);
let userAddress = $derived($authState.address);

// Conditional rendering
{#if isLoading}
  <p>Loading...</p>
{:else if isAuthenticated}
  <p>Welcome, {userAddress}</p>
  <button onclick={() => authState.logout()}>Disconnect</button>
{:else}
  <button onclick={() => authState.signIn()}>Connect Wallet</button>
{/if}

Verify Authentication Endpoint

Optional endpoint to check if session is still valid:
# backend/api/views.py

def verify_auth(request):
    """Check if request has valid authentication."""
    ethereum_address = request.session.get('ethereum_address')
    authenticated = request.session.get('authenticated', False)
    
    if authenticated and ethereum_address:
        return JsonResponse({
            'authenticated': True,
            'address': ethereum_address
        })
    
    return JsonResponse({'authenticated': False})

Logout Flow

Frontend

async function logout() {
  try {
    await axios.post('/api/auth/logout/', {}, { withCredentials: true });
    authState.set({ isAuthenticated: false, address: null });
    userStore.clearUser();
  } catch (error) {
    console.error('Logout failed:', error);
  }
}

Backend

def logout(request):
    """Clear authentication session."""
    request.session.flush()  # Clear all session data
    return JsonResponse({'success': True})

Protected Routes

Backend - Require Authentication

from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated

@api_view(['GET', 'PATCH'])
@permission_classes([IsAuthenticated])
def user_profile(request):
    """Get or update current user profile."""
    user = request.user  # Automatically set by authentication class
    
    if request.method == 'GET':
        serializer = UserSerializer(user)
        return Response(serializer.data)
    
    elif request.method == 'PATCH':
        serializer = UserSerializer(user, data=request.data, partial=True)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=400)

Frontend - Protect Routes

// In a Svelte component
import { authState } from '../lib/auth.js';
import { push } from 'svelte-spa-router';

$effect(() => {
  if (!$authState.loading && !$authState.isAuthenticated) {
    // Redirect to home if not authenticated
    push('/');
  }
});

Security Considerations

  • Session cookies use SameSite=Lax
  • CSRF exemption only for authentication endpoints
  • All other endpoints require CSRF token
  • Session cookies are httpOnly (not accessible via JavaScript)
  • No sensitive data in localStorage
  • Content Security Policy headers
  • Nonce is single-use (deleted after verification)
  • Messages include timestamp
  • Session expires after 2 weeks
  • Signature cryptographically proves wallet ownership
  • Case-insensitive address comparison
  • Address stored in lowercase for consistency

Troubleshooting

MetaMask Not Detected

Problem: window.ethereum is undefined Solution:
  • Install MetaMask browser extension
  • Refresh the page after installation
  • Check for conflicts with other wallet extensions

Signature Verification Failed

Problem: Backend returns “Verification failed” Solutions:
  • Ensure nonce matches between frontend and backend
  • Check that SIWE message format is correct
  • Verify signature was generated for the correct message
  • Check that address is lowercased consistently

Session Not Persisted

Problem: User gets logged out on page refresh Solutions:
  • Verify withCredentials: true in Axios config
  • Check CORS configuration allows credentials
  • Ensure session cookie is not being blocked by browser
  • Check SESSION_COOKIE_SECURE setting matches HTTPS usage

CORS Errors

Problem: Browser blocks requests with credentials Solutions:
# backend/settings.py
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
    'http://localhost:5173',
    'https://your-frontend-domain.com'
]

Testing Authentication

Manual Testing

  1. Open frontend in browser
  2. Open browser DevTools (F12)
  3. Go to Console tab
  4. Click “Connect Wallet”
  5. Check Network tab for API calls
  6. Verify session cookie in Application tab

Automated Testing

Example test:
# backend/api/tests/test_auth.py
from django.test import TestCase
from unittest.mock import patch

class AuthenticationTests(TestCase):
    def test_get_nonce(self):
        response = self.client.get('/api/auth/nonce/', {'address': '0x123...'})
        self.assertEqual(response.status_code, 200)
        self.assertIn('nonce', response.json())
    
    @patch('api.views.SiweMessage')
    def test_login_success(self, mock_siwe):
        # Mock SIWE verification
        mock_siwe.from_message.return_value.verify.return_value = True
        
        # Simulate login
        response = self.client.post('/api/auth/login/', {
            'address': '0x123...',
            'message': 'test message',
            'signature': '0xabc...'
        })
        
        self.assertEqual(response.status_code, 200)
        self.assertTrue(response.json()['success'])

Next Steps

Backend Setup

Set up Django backend

Frontend Setup

Set up Svelte frontend

Environment Variables

Configure authentication settings

Deployment

Deploy to production

Build docs developers (and LLMs) love