| Crates.io | mero-auth |
| lib.rs | mero-auth |
| version | 0.2.5 |
| created_at | 2025-09-09 14:10:57.783542+00 |
| updated_at | 2025-09-09 14:10:57.783542+00 |
| description | Forward Authentication Service for Calimero Network |
| homepage | |
| repository | https://github.com/calimero-network/core |
| max_upload_size | |
| id | 1830936 |
| size | 499,157 |
A production-ready forward authentication service implementing hierarchical key management with pluggable providers and comprehensive security features.
The service implements Forward Authentication pattern where a reverse proxy delegates authentication decisions to this service. The node remains completely authentication-unaware.
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────┐
│ Client │───▶│ Reverse Proxy│───▶│ Auth Service│ │ Node │
│ │ │ (nginx/ │ │ │ │ │
│ │ │ traefik) │ │ │ │ │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────┘
│ │
│ │
▼ ▼
/auth/validate JWT validation
(token check) & permission check
# Run with minimal configuration
cargo run --bin calimero-auth -- --bind 0.0.0.0:3001
# Test endpoints
curl http://localhost:3001/auth/identity
curl http://localhost:3001/auth/providers
# With configuration file
cargo run --bin calimero-auth -- --config config.toml
config.toml)listen_addr = "0.0.0.0:3001"
[jwt]
issuer = "calimero-auth"
access_token_expiry = 3600 # 1 hour
refresh_token_expiry = 2592000 # 30 days
[storage]
type = "rocksdb" # or "memory"
path = "/data/auth_db"
[cors]
allow_all_origins = true
allowed_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allowed_headers = ["Authorization", "Content-Type", "Accept"]
[security]
max_body_size = 1048576 # 1MB
[security.rate_limit]
rate_limit_rpm = 50
rate_limit_burst = 5
[security.headers]
enabled = true
hsts_max_age = 31536000
frame_options = "DENY"
[providers]
near_wallet = true
[near]
network = "testnet"
rpc_url = "https://rpc.testnet.near.org"
wallet_url = "https://wallet.testnet.near.org"
All configuration can be overridden via environment variables using AUTH_ prefix:
AUTH_LISTEN_ADDR="0.0.0.0:3001"
AUTH_JWT__ISSUER="my-issuer"
AUTH_STORAGE__TYPE="rocksdb"
AUTH_STORAGE__PATH="/data/auth"
AUTH_PROVIDERS__NEAR_WALLET=true
The service implements a two-tier key system:
Root Keys - Identity-level authentication
Client Keys - Context-scoped authorization
graph TD
A[User Authentication] --> B[Root Key Created]
B --> C[Root Key Permissions Set]
C --> D[Client Key Request]
D --> E[Permission Validation]
E --> F[Client Key Generated]
F --> G[Context-Scoped Access]
Keys:
├── root:{key_id} # Root key data
├── client:{key_id} # Client key data
├── root_clients:{root_id} # Client key index
└── permissions:{key_id} # Key permissions
# Get challenge
curl -s http://localhost:3001/auth/challenge
# Sign challenge with wallet (browser/CLI)
# Exchange for root key JWT
curl -X POST http://localhost:3001/auth/token \
-H 'Content-Type: application/json' \
-d '{
"auth_method": "near_wallet",
"public_key": "ed25519:...",
"client_name": "My App",
"timestamp": 1234567890,
"provider_data": {
"wallet_address": "user.testnet",
"signature": "...",
"message": "challenge"
}
}'
# Use root key to generate client key
curl -X POST http://localhost:3001/admin/client-key \
-H "Authorization: Bearer $ROOT_KEY_JWT" \
-d '{
"context_id": "01J7XS...",
"context_identity": "user.testnet",
"permissions": ["context:read", "context:execute"]
}'
GET /auth/login # React SPA for authentication
GET /auth/challenge # Get signing challenge
POST /auth/token # Exchange challenge for root key JWT
POST /auth/refresh # Refresh expired tokens
GET /auth/providers # List available providers
GET /auth/identity # Service information
GET /auth/validate # Forward auth validation endpoint
POST /auth/validate # (same as above, for proxies)
# Root Key Management
GET /admin/keys # List root keys
POST /admin/keys # Create root key
DELETE /admin/keys/{key_id} # Delete root key
# Client Key Management
GET /admin/keys/clients # List client keys
POST /admin/client-key # Generate client key
DELETE /admin/keys/{key_id}/clients/{client_id} # Delete client key
# Permission Management
GET /admin/keys/{key_id}/permissions # Get key permissions
PUT /admin/keys/{key_id}/permissions # Update permissions
# Token Management
POST /admin/revoke # Revoke current token
# System
GET /admin/metrics # Service metrics
pub enum Permission {
Keys(KeyPermission), // Key management
Context(ContextPermission), // Context operations
Application(ApplicationPermission), // Application management
Alias(AliasPermission), // Alias management
Blob(BlobPermission), // Blob operations
}
{
"permissions": [
"keys:create",
"keys:list",
"context:read:global",
"context:execute:specific:01J7XS...",
"application:install:global",
"alias:create:specific:my-alias"
]
}
The service validates permissions for each request by:
// Example mappings in validator.rs
"/admin/keys" + GET -> Permission::Keys(KeyPermission::List)
"/admin/keys" + POST -> Permission::Keys(KeyPermission::Create)
"/admin/contexts/{id}" -> Permission::Context(ContextPermission::Read(Specific(id)))
#[async_trait]
pub trait AuthProvider: Send + Sync {
fn name(&self) -> &str;
fn provider_type(&self) -> &str;
fn description(&self) -> &str;
fn supports_method(&self, method: &str) -> bool;
fn is_configured(&self) -> bool;
async fn authenticate(&self, request: &AuthRequest) -> Result<AuthResponse>;
async fn verify_signature(&self, data: &VerificationData) -> Result<bool>;
fn get_config_options(&self) -> serde_json::Value;
}
near_wallet)src/providers/impls/near_wallet.rs// src/providers/impls/my_provider.rs
pub struct MyProvider {
config: MyConfig,
}
#[async_trait]
impl AuthProvider for MyProvider {
fn name(&self) -> &str { "my_provider" }
async fn authenticate(&self, request: &AuthRequest) -> Result<AuthResponse> {
// 1. Extract provider_data from request
// 2. Verify signature/credentials
// 3. Return AuthResponse with user info
}
async fn verify_signature(&self, data: &VerificationData) -> Result<bool> {
// Implement signature verification logic
}
}
// src/providers/mod.rs
pub fn create_providers(
storage: Arc<dyn Storage>,
config: &AuthConfig,
token_manager: TokenManager,
) -> Result<Vec<Box<dyn AuthProvider>>> {
let mut providers = Vec::new();
if config.providers.get("near_wallet").unwrap_or(&false) {
providers.push(Box::new(NearWalletProvider::new(config.near.clone())?));
}
// Add your provider here
if config.providers.get("my_provider").unwrap_or(&false) {
providers.push(Box::new(MyProvider::new(config.my_config.clone())?));
}
Ok(providers)
}
#[async_trait]
pub trait Storage: Send + Sync {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
async fn set(&self, key: &str, value: &[u8]) -> Result<()>;
async fn delete(&self, key: &str) -> Result<()>;
async fn exists(&self, key: &str) -> Result<bool>;
async fn list_keys(&self, prefix: &str) -> Result<Vec<String>>;
}
[storage]
type = "rocksdb"
path = "/data/auth_db"
[storage]
type = "memory"
pub struct MyStorage {
// Your storage implementation
}
#[async_trait]
impl Storage for MyStorage {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
// Implement get logic
}
async fn set(&self, key: &str, value: &[u8]) -> Result<()> {
// Implement set logic
}
// ... other methods
}
The service automatically generates and manages JWT signing secrets:
Secrets:
├── system:secrets:jwt_auth # JWT auth signing secret
├── system:secrets:jwt_challenge # JWT challenge signing secret
├── system:secrets:csrf # CSRF protection secret
├── system:secrets:jwt_auth_backup # Backup auth secret
├── system:secrets:jwt_challenge_backup # Backup challenge secret
└── system:secrets:csrf_backup # Backup CSRF secret
impl SecretManager {
pub async fn initialize(&self) -> Result<()>;
pub async fn get_secret(&self, secret_type: SecretType) -> Result<String>;
pub async fn rotate_secret(&self, secret_type: SecretType) -> Result<()>;
pub async fn get_jwt_auth_secret(&self) -> Result<String>;
pub async fn get_jwt_challenge_secret(&self) -> Result<String>;
pub async fn get_csrf_secret(&self) -> Result<String>;
}
All handlers follow this pattern:
pub async fn handler_name(
state: Extension<Arc<AppState>>,
// Other extractors (Query, Json, Path, etc.)
) -> impl IntoResponse {
// 1. Validate input
// 2. Check permissions (if protected)
// 3. Execute business logic
// 4. Return standardized response
}
{
"data": { /* response data */ },
"error": null
}
{
"data": null,
"error": "Error message"
}
src/api/handlers/auth.rs)login_handler: Serves React SPA for interactive authchallenge_handler: Generates signing challengestoken_handler: Exchanges signed challenge for JWTrefresh_token_handler: Refreshes expired tokensvalidate_handler: Validates tokens for forward authsrc/api/handlers/root_keys.rs, src/api/handlers/client_keys.rs)list_keys_handler: Lists user's keyscreate_key_handler: Creates new root keysdelete_key_handler: Revokes keysgenerate_client_key_handler: Creates client keyssrc/api/handlers/permissions.rs)get_key_permissions_handler: Gets key permissionsupdate_key_permissions_handler: Updates permissions# Unit tests
cargo test
# Integration tests
cargo test --test integration
# Specific test module
cargo test auth::token
# Enable debug logging
RUST_LOG=debug cargo run --bin calimero-auth
# Test with curl
curl -v http://localhost:3001/auth/identity
cd frontend
npm install
npm run dev # Starts on http://localhost:5173
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release --bin calimero-auth
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /app/target/release/calimero-auth /usr/local/bin/
EXPOSE 3001
CMD ["calimero-auth", "--config", "/config/auth.toml"]
server {
listen 80;
server_name api.example.com;
# Auth service routes
location /auth/ {
proxy_pass http://auth:3001;
}
location /admin/ {
proxy_pass http://auth:3001;
}
# Protected routes (forward auth)
location / {
auth_request /auth-check;
proxy_pass http://node:2428;
}
location = /auth-check {
internal;
proxy_pass http://auth:3001/auth/validate;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
}
# docker-compose.yml
services:
auth:
image: calimero/auth:latest
labels:
- "traefik.http.routers.auth.rule=PathPrefix(`/auth`) || PathPrefix(`/admin`)"
- "traefik.http.middlewares.auth-forward.forwardauth.address=http://auth:3001/auth/validate"
node:
image: calimero/node:latest
labels:
- "traefik.http.routers.node.rule=!PathPrefix(`/auth`) && !PathPrefix(`/admin`)"
- "traefik.http.routers.node.middlewares=auth-forward"
// Automatically added security headers
"Strict-Transport-Security": "max-age=31536000; includeSubDomains"
"X-Frame-Options": "DENY"
"X-Content-Type-Options": "nosniff"
"Referrer-Policy": "strict-origin-when-cross-origin"
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline'"
# Service health
curl http://localhost:3001/auth/health
# Service identity
curl http://localhost:3001/auth/identity
curl -H "Authorization: Bearer $JWT" \
http://localhost:3001/admin/metrics
# Configure logging levels
RUST_LOG=calimero_auth=debug,tower_http=info cargo run --bin calimero-auth
"No authentication providers available"
"Storage initialization failed"
"JWT secret generation failed"
"Forward auth validation failed"
RUST_LOG=trace cargo run --bin calimero-auth -- --config config.toml
Licensed under MIT OR Apache-2.0