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
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 };
}
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 ;
}
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 };
}
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 ;
}
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
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})
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 )
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
Configure in settings
# backend/settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES' : [
'ethereum_auth.authentication.EthereumAuthentication' ,
],
'DEFAULT_PERMISSION_CLASSES' : [
'rest_framework.permissions.IsAuthenticatedOrReadOnly' ,
],
}
Session Management
Session Cookie Configuration
# 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
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
Open frontend in browser
Open browser DevTools (F12)
Go to Console tab
Click “Connect Wallet”
Check Network tab for API calls
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