- Overview
- Architecture
- Privacy Guarantees
- Implementation Guide
- API Reference
- Security Considerations
- RBAC Implementation
Soulbound Auth (SBA) is a revolutionary Web3 authentication system that achieves perfect privacy by never storing Ethereum addresses on the server. Instead, it uses non-transferable Soulbound Tokens (SBTs) combined with cryptographic proofs to maintain user identity.
- Zero Address Storage: Server databases never contain wallet addresses
- Ephemeral Authentication: Each session validates ownership through signatures
- Privacy by Design: Only hashed Account IDs are stored
- No External Dependencies: No email, phone, or OAuth required
┌────────────────────────────────────────────────────┐
│ User Wallet │
│ • Signs authentication messages │
│ • Holds SBT (non-transferable) │
│ • Never reveals private key │
└────────────────┬───────────────────────────────────┘
│
┌────────────────▼───────────────────────────────────┐
│ SBA Client Library │
│ • Manages wallet connection │
│ • Handles signature generation │
│ • Manages JWT tokens │
│ • Interfaces with SBT contract │
└────────────────┬───────────────────────────────────┘
│
┌────────────────▼───────────────────────────────────┐
│ SBA Server │
│ • Validates signatures │
│ • Generates/validates JWTs │
│ • Stores Account IDs (NOT addresses) │
│ • Manages RBAC │
└────────────────┬───────────────────────────────────┘
│
┌────────────────▼───────────────────────────────────┐
│ SBT Smart Contract │
│ • Mints non-transferable tokens │
│ • Generates unique Account IDs │
│ • Stores EULA acceptance │
│ • Provides on-chain verification │
└────────────────────────────────────────────────────┘
// User connects wallet
await ethereum.request({ method: 'eth_requestAccounts' });
// Sign authentication message
const message = generateAuthMessage();
const signature = await signer.signMessage(message);
// Server validates signature and checks for SBT
const jwt = await server.authenticate(address, signature, message);
// Returns: { hasSBT: false, accountId: null }// User accepts EULA
const eulaHash = keccak256(eulaText);
// Mint SBT on-chain
const tx = await sbtContract.mintSBT(eulaHash, version);
// Server creates account with Account ID (no address!)
await server.confirmMint(txHash, eulaHash);
// Returns: { hasSBT: true, accountId: "0x..." }// User signs new message
const signature = await signer.signMessage(message);
// Server validates and retrieves Account ID from chain
const jwt = await server.authenticate(address, signature, message);
// Returns: { hasSBT: true, accountId: "0x..." }- ✅ Hashed Account IDs (non-reversible)
- ✅ EULA acceptance records
- ✅ Role assignments
- ✅ Application-specific metadata
- ❌ Ethereum addresses
- ❌ Transaction histories
- ❌ Wallet balances
- ❌ Personal identifiable information
The Account ID is generated using multiple layers of hashing:
function _generateAccountId(address owner, uint256 tokenId)
private pure returns (bytes32)
{
bytes32 layer1 = keccak256(abi.encodePacked(owner, tokenId));
bytes32 layer2 = keccak256(abi.encodePacked(layer1, "SBA_PRIVACY_SALT_V1"));
return keccak256(abi.encodePacked(
uint128(uint256(layer2)),
uint128(uint256(layer1)),
"SOULBOUND_AUTH"
));
}This ensures:
- Account IDs cannot be reversed to addresses
- Each ID is unique and deterministic
- No correlation between IDs and addresses
# Install dependencies
npm install
# Deploy to local network
npx hardhat node
npx hardhat run scripts/deploy.js --network localhost
# Deploy to testnet
npx hardhat run scripts/deploy.js --network sepolia// server.js
const SoulboundAuthServer = require('soulbound-auth/server');
const server = new SoulboundAuthServer({
sbtContractAddress: '0x...', // Your deployed SBT
rpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/...',
jwtSecret: 'your-secure-secret',
adminSBTIds: ['0x...'] // Optional admin Account IDs
});
server.start();// client.js
import { SoulboundAuthClient } from 'soulbound-auth/client';
const sbaClient = new SoulboundAuthClient({
serverUrl: 'https://api.yourapp.com',
sbtContractAddress: '0x...'
});
// Connect and authenticate
const auth = await sbaClient.connect();
// Mint SBT if needed
if (!auth.hasSBT) {
const eulaHash = generateEulaHash();
await sbaClient.mintSBT(eulaHash, 1);
}Authenticate a user with signature verification.
Request:
{
"address": "0x...",
"signature": "0x...",
"message": "Soulbound Auth\n\n..."
}Response:
{
"token": "jwt...",
"hasSBT": true,
"accountId": "0x...",
"role": "user"
}Confirm SBT minting and create account.
Request:
{
"address": "0x...",
"signature": "0x...",
"eulaHash": "0x...",
"version": 1,
"txHash": "0x..."
}Get account information (requires JWT).
Headers:
Authorization: Bearer <jwt>
Response:
{
"account_id": "0x...",
"created_at": 1234567890,
"role": "user",
"permissions": ["read", "write"]
}Mint a new Soulbound Token.
Returns:
tokenId: The minted token IDaccountId: The generated Account ID
Get account information for an address.
Returns:
hasToken: Whether the address has an SBTaccountId: The Account ID if existstokenId: The token ID if exists
- Always verify signatures server-side
- Use time-based nonces to prevent replay attacks
- Implement rate limiting
- Use strong, random secrets
- Set appropriate expiration times
- Rotate secrets regularly
- SBTs are non-transferable by design
- Account IDs use strong cryptographic hashing
- EULA hashes provide legal compliance
- Never store addresses, even encrypted
- Use Account IDs as primary keys
- Implement proper access controls
// Define roles by Account ID
const ROLES = {
admins: [
'0xabc123...', // Account ID, not address!
'0xdef456...'
],
moderators: [
'0x789ghi...'
]
};
// Check role in middleware
function requireRole(role) {
return (req, res, next) => {
const accountId = req.user.accountId;
if (ROLES[role].includes(accountId)) {
next();
} else {
res.status(403).json({ error: 'Insufficient permissions' });
}
};
}
// Use in routes
app.get('/admin', requireAuth, requireRole('admins'), (req, res) => {
// Admin-only endpoint
});-- Store permissions by Account ID
CREATE TABLE permissions (
account_id TEXT PRIMARY KEY,
can_read BOOLEAN DEFAULT true,
can_write BOOLEAN DEFAULT false,
can_delete BOOLEAN DEFAULT false,
can_admin BOOLEAN DEFAULT false
);- Never Log Addresses: Even in development, avoid logging wallet addresses
- Use Standard Auth Messages: Consistent message format prevents phishing
- Implement Rate Limiting: Protect against brute force attacks
- Regular Token Rotation: Refresh JWTs regularly
- Audit Trail: Log actions by Account ID, not address
Issue: "Already has SBT" error
- Each address can only mint one SBT
- This is by design for identity uniqueness
Issue: JWT expired
- Implement automatic refresh in client
- Use refresh endpoint before expiration
Issue: Account not found
- Ensure SBT was minted successfully
- Verify mint confirmation was called
MIT License - See LICENSE file for details
Contributions welcome! Please submit PRs to the GitHub repository.
- GitHub Issues: https://github.com/your-org/soulbound-auth/issues
- Discord: https://discord.gg/soulbound-auth
- Documentation: https://docs.soulbound-auth.io