Authentication
SIWE authentication and session management
The API uses SIWE (Sign-In with Ethereum) for wallet-based authentication. Sessions are managed via secure cookies.
Overview
FRONTEND WALLET API SERVER
│ │ │
├── 1. Request nonce ───────────────────────────────►│
│ │ │
│◄── 2. Return nonce ────────────────────────────────┤
│ │ │
├── 3. Create SIWE message ─►│ │
│ │ │
│◄── 4. User signs ─────────┤ │
│ │ │
├── 5. Verify signature ────────────────────────────►│
│ │ Server verifies │
│ │ and creates session │
│◄── 6. Session cookie ──────────────────────────────┤
│ │ │
├── 7. Authenticated requests ──────────────────────►│
│ (Protected endpoints) │SIWE Message Format
SIWE messages follow EIP-4361:
diffusal.xyz wants you to sign in with your Ethereum account:
0x1234...abcd
Sign in to Diffusal
URI: https://api.diffusal.xyz
Version: 1
Chain ID: 1
Nonce: k8j3h2g1f0e9d8c7b6a5
Issued At: 2024-01-15T12:00:00.000Z
Expiration Time: 2024-01-15T12:15:00.000ZMessage Fields
| Field | Description |
|---|---|
domain | The domain requesting authentication |
address | User's Ethereum address |
statement | Human-readable sign-in statement |
uri | API server URI |
version | SIWE version (always "1") |
chainId | Ethereum chain ID |
nonce | Server-generated random nonce |
issuedAt | Message creation timestamp |
expirationTime | Message expiry (15 minutes) |
Authentication Flow
Step 1: Request Nonce
GET /api/auth/siwe/nonce?walletAddress=0x1234...abcdResponse:
| Field | Type | Description |
|---|---|---|
nonce | string | 32-character random nonce |
Notes:
- Nonce is valid for 15 minutes
- Each nonce can only be used once
- Request a new nonce for each sign-in attempt
Step 2: Create and Sign Message
The frontend creates a SIWE message with the nonce and prompts the user to sign.
Message Parameters:
| Parameter | Value |
|---|---|
domain | diffusal.xyz |
address | User's connected wallet address |
statement | "Sign in to Diffusal" |
uri | https://api.diffusal.xyz |
version | "1" |
chainId | Connected chain ID |
nonce | Nonce from Step 1 |
issuedAt | Current ISO timestamp |
expirationTime | 15 minutes from now |
Domain Configuration
Important: The domain and uri fields serve different purposes:
| Field | Purpose | Example |
|---|---|---|
domain | Frontend origin where sign-in is initiated | diffusal.xyz |
uri | API server endpoint | https://api.diffusal.xyz |
By Environment:
| Environment | domain | uri |
|---|---|---|
| Production | diffusal.xyz | https://api.diffusal.xyz |
| Local Dev | localhost | http://localhost:8080 |
Common Mistake: Using api.diffusal.xyz as the domain. The domain should match your frontend's origin, not the API server.
// Correct for production frontend at diffusal.xyz
const message = createSiweMessage({
domain: "diffusal.xyz", // Frontend domain
uri: "https://api.diffusal.xyz", // API endpoint
// ...
});
// Correct for local development (frontend at localhost:3000)
const message = createSiweMessage({
domain: "localhost",
uri: "http://localhost:8080",
// ...
});The server validates that the domain matches one of the trusted origins configured in Better Auth.
Step 3: Verify Signature
POST /api/auth/siwe/verify
Content-Type: application/json
{
"message": "diffusal.xyz wants you to sign in...",
"signature": "0x..."
}Request Body:
| Field | Type | Description |
|---|---|---|
message | string | Complete SIWE message |
signature | string | Wallet signature |
Response (Success):
| Field | Type | Description |
|---|---|---|
user | object | User profile |
session | object | Session details |
Response Headers:
Set-Cookie: session=eyJhbGc...; HttpOnly; Secure; SameSite=Strict; Path=/Step 4: Use Authenticated Endpoints
Include the session cookie in subsequent requests:
GET /account/me
Cookie: session=eyJhbGc...Or use the Authorization header:
GET /account/me
Authorization: Bearer eyJhbGc...Session Management
Session Properties
| Property | Value |
|---|---|
| Duration | 7 days |
| Storage | Server-side (database) |
| Token Type | JWT (in cookie) |
| Update Age | 24 hours (refreshed daily) |
| Cookie Cache | 5 minutes (for performance) |
Check Session
GET /api/auth/sessionResponse (Authenticated):
| Field | Type | Description |
|---|---|---|
user.id | string | User ID |
user.address | string | Wallet address |
user.name | string | ENS name (if available) |
session.id | string | Session ID |
session.expiresAt | string | Expiry timestamp |
Response (Not Authenticated):
| Field | Type | Description |
|---|---|---|
user | null | No user |
session | null | No session |
Sign Out
POST /api/auth/sign-outEffects:
- Invalidates current session
- Clears session cookie
- User must re-authenticate
Security Considerations
Nonce Security
| Property | Implementation |
|---|---|
| Generation | Cryptographically random |
| Length | 32 characters |
| Lifetime | 15 minutes |
| Single-use | Invalidated after verification |
Cookie Security
| Attribute | Value | Purpose |
|---|---|---|
HttpOnly | true | Prevent XSS access |
Secure | true | HTTPS only |
SameSite | Strict | Prevent CSRF |
Path | / | All routes |
Rate Limits
| Endpoint | Limit | Window |
|---|---|---|
/api/auth/siwe/nonce | 10 | 1 minute |
/api/auth/siwe/verify | 5 | 1 minute |
/api/auth/sign-out | 10 | 1 minute |
Common Attacks Prevented
| Attack | Prevention |
|---|---|
| Replay Attack | Single-use nonces |
| CSRF | SameSite cookies |
| XSS Token Theft | HttpOnly cookies |
| Session Fixation | New session on auth |
| Brute Force | Rate limiting |
Error Handling
Verification Errors
| Error Code | Description | Solution |
|---|---|---|
invalid_nonce | Nonce expired or used | Request new nonce |
invalid_signature | Signature doesn't match | Re-sign message |
address_mismatch | Signed with wrong wallet | Use correct wallet |
expired_message | Message past expiration | Create new message |
invalid_domain | Domain doesn't match | Check SIWE config |
Session Errors
| HTTP Status | Description | Solution |
|---|---|---|
| 401 | No valid session | Re-authenticate |
| 403 | Session expired | Re-authenticate |
Multiple Wallets
Users can link multiple wallets to a single account:
Check Linked Wallets
GET /auth/api/walletsResponse:
| Field | Type | Description |
|---|---|---|
wallets | array | List of wallet info |
total | number | Total wallet count |
Wallet Object:
| Field | Type | Description |
|---|---|---|
address | string | Ethereum wallet address |
isPrimary | boolean | True if primary wallet |
linkedAt | string | ISO timestamp when linked |
Get Primary Wallet
GET /auth/api/wallets/primaryResponse:
| Field | Type | Description |
|---|---|---|
address | string | Primary wallet address |
userId | string | User identifier |
Verify Wallet Ownership
Verify a wallet address using SIWE signature:
POST /auth/api/wallets/verify
{
"message": "...",
"signature": "...",
"address": "0x..."
}ENS Integration
ENS names are resolved during authentication:
| Field | Source |
|---|---|
user.name | ENS primary name |
user.avatar | ENS avatar record |
ENS lookups are optional and non-blocking. If ENS resolution fails, the wallet address is used as the display name.
WebSocket Authentication
WebSocket connections use the same JWT token:
1. Connect to ws://api.diffusal.xyz/ws
2. Send auth message:
{
"type": "auth",
"token": "eyJhbGc..."
}
3. Receive confirmation:
{
"type": "auth_success",
"address": "0x..."
}See WebSocket API for details.
Related
- API Reference - Account endpoints
- WebSocket - Real-time authentication