Package Exports
- @rodit/rodit-auth-be
Readme
RODiT Authentication SDK
A comprehensive Node.js SDK for implementing RODiT-based mutual authentication, authorization, self-configuration, and session management in Express.js applications.
Version: 4.0.2
License: Proprietary
Author: Discernible Inc.
Table of Contents
- Quick Start
- Core Concepts
- Installation & Setup
- Authentication
- Authorization & Permissions
- Session Management
- Configuration
- Logging & Monitoring
- Performance Tracking
- Performance Tuning
- Webhooks
- Advanced Usage
- API Reference
- Best Practices
- Security Considerations
- Troubleshooting
- Version History
Quick Start
Installation
npm install @rodit/rodit-auth-beBasic Server Setup
const express = require('express');
const { RoditClient, setExpressSessionStore } = require('@rodit/rodit-auth-be');
const { ulid } = require('ulid');
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
const app = express();
let roditClient;
// Configure session storage BEFORE initializing RoditClient
const sessionStore = new SQLiteStore({
db: 'sessions.db',
dir: './data',
table: 'sessions'
});
setExpressSessionStore(sessionStore);
// Configure Express middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Request context middleware
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || ulid();
req.startTime = Date.now();
next();
});
// Server startup with SDK initialization
async function startServer() {
try {
// Initialize RODiT client (use 'server' for server applications)
roditClient = await RoditClient.create('server');
// Store client in app.locals for route access
app.locals.roditClient = roditClient;
// Get logger and other services from client
const logger = roditClient.getLogger();
const config = roditClient.getConfig();
const loggingmw = roditClient.getLoggingMiddleware();
// Apply logging middleware
app.use(loggingmw);
// Create authentication middleware
const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
const authorize = (req, res, next) => roditClient.authorize(req, res, next);
// Public routes
app.post('/api/login', (req, res) => {
req.logAction = 'login-attempt';
return roditClient.login_client(req, res);
});
// Protected routes
app.post('/api/logout', authenticate, (req, res) => {
req.logAction = 'logout-attempt';
return roditClient.logout_client(req, res);
});
app.get('/api/protected', authenticate, (req, res) => {
res.json({ message: 'Protected data', user: req.user });
});
// Protected + authorized routes
app.use('/api/admin', authenticate, authorize, adminRoutes);
const port = config.get('SERVERPORT', 3000);
app.listen(port, () => {
logger.info(`RODiT Authentication Server running on port ${port}`);
});
} catch (error) {
console.error('Server initialization failed:', error);
process.exit(1);
}
}
startServer();Core Concepts
The RoditClient Pattern
The SDK centers around the RoditClient class, which provides a unified interface for all RODiT operations:
- Single Initialization: Create once with
RoditClient.create(role)where role is'server','client', or'portal' - Shared Instance: Store in
app.localsfor access across routes and middleware - Self-Configuring: Automatically loads configuration from Vault, files, or environment variables
- Encapsulated: All SDK functionality accessed through the client instance
- Session Management: Built-in session tracking with pluggable storage backends
- Performance Monitoring: Integrated request tracking and metrics collection
App.locals Pattern
Store the initialized client in app.locals for consistent access across your application:
// In main app.js
roditClient = await RoditClient.create('server');
app.locals.roditClient = roditClient;
// In route modules
const router = express.Router();
router.get('/data', (req, res) => {
const client = req.app.locals.roditClient;
const logger = client.getLogger();
logger.info('Processing request', {
component: 'DataRoute',
userId: req.user?.id
});
res.json({ data: 'example' });
});Authentication Middleware Pattern
Create middleware functions that delegate to the RoditClient:
// Create reusable middleware
const authenticate = (req, res, next) => {
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({ error: 'Authentication service unavailable' });
}
return client.authenticate(req, res, next);
};
const authorize = (req, res, next) => {
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({ error: 'Authorization service unavailable' });
}
return client.authorize(req, res, next);
};
// Use in routes
app.get('/api/protected', authenticate, handler);
app.post('/api/admin', authenticate, authorize, adminHandler);Installation & Setup
Dependencies
Required:
npm install @rodit/rodit-auth-be express config winstonRecommended for Production:
npm install express-session connect-sqlite3Optional:
npm install node-vault # For Vault-based credentials
npm install winston-loki # For Grafana Loki loggingEnvironment Variables
Vault Configuration (Production):
export RODIT_NEAR_CREDENTIALS_SOURCE=vault
export VAULT_ENDPOINT=https://vault.example.com
export VAULT_ROLE_ID=your-role-id
export VAULT_SECRET_ID=your-secret-id
export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
export SERVICE_NAME=your-service-name
export NEAR_CONTRACT_ID=your-contract.testnetApplication Configuration:
export SERVERPORT=3000
export NODE_ENV=production # Environment: production, development, test
export LOG_LEVEL=info # Logging: error, warn, info, debug, trace
export API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.sqliteLogging Configuration:
export LOKI_URL=https://loki.example.com:3100
export LOKI_BASIC_AUTH=username:passwordConfiguration Files
Create config/default.json:
{
"NEAR_CONTRACT_ID": "your-contract.testnet",
"SERVICE_NAME": "your-service",
"SERVERPORT": 3000,
"API_DEFAULT_OPTIONS": {
"LOG_DIR": "/app/logs",
"DB_PATH": "/app/data/database.sqlite"
},
"SECURITY_OPTIONS": {
"SILENT_LOGIN_FAILURES": false,
"JWT_DURATION": 3600
}
}Authentication
RODiT-Based Authentication
RODiT provides cryptographic mutual authentication using blockchain-verified identities.
Client Login Request
Clients authenticate by sending RODiT credentials:
// POST /api/login
{
"roditid": "01K4G3D95QF6NR0RSJK9WEK6KA",
"timestamp": 1640995200,
"roditid_base64url_signature": "base64url-encoded-signature"
}Server Response
// Success (200)
{
"message": "Login successful",
"token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...",
"requestId": "01HQXYZ123ABC"
}
// Headers:
// Authorization: Bearer eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...Authentication Flow
- Client sends RODiT credentials - RODiT ID, timestamp, and cryptographic signature
- SDK verifies signature - Validates against blockchain records (NEAR Protocol)
- Session created - New session stored in session manager
- JWT token issued - Token contains session ID and user claims
- Subsequent requests - Client sends JWT in
Authorization: Bearer <token>header - Token validation - SDK validates JWT and checks session status
Login Implementation
// routes/login.js
const express = require('express');
const router = express.Router();
router.post('/login', async (req, res) => {
req.logAction = 'login-attempt';
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({ error: 'Authentication service unavailable' });
}
// Delegate to SDK's login_client method
await client.login_client(req, res);
});
module.exports = router;Logout Implementation
// Logout invalidates the JWT token and closes the session
router.post('/logout', authenticate, async (req, res) => {
req.logAction = 'logout-attempt';
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({ error: 'Authentication service unavailable' });
}
// Delegate to SDK's logout_client method
await client.logout_client(req, res);
});Protected Routes
// Require authentication for access
app.get('/api/data', authenticate, (req, res) => {
// req.user contains authenticated user information
const logger = req.app.locals.roditClient.getLogger();
logger.info('Protected route accessed', {
component: 'API',
userId: req.user.id,
roditId: req.user.roditId,
requestId: req.requestId
});
res.json({
message: 'Authenticated data',
user: req.user,
requestId: req.requestId
});
});Authentication Middleware
The authenticate middleware validates JWT tokens and populates req.user:
const authenticate = (req, res, next) => {
const client = req.app.locals.roditClient;
return client.authenticate(req, res, next);
};
// After successful authentication, req.user contains:
// {
// id: 'user-unique-id',
// roditId: '01K4G3D95QF6NR0RSJK9WEK6KA',
// aud: 'audience',
// iss: 'issuer',
// exp: 1640999999,
// iat: 1640995200,
// session_id: '01HQXYZ123ABC'
// }Authorization & Permissions
Route-Based Permissions
Permissions are configured in your RODiT token metadata using the permissioned_routes field:
{
"permissioned_routes": {
"entities": {
"/": {
"methods": "+0"
},
"/api/echo": {
"methods": "+0"
},
"/api/cruda/create": {
"methods": "+0"
},
"/api/cruda/list": {
"methods": "+0"
},
"/api/admin": {
"methods": "+0"
}
}
}
}Permission Format:
"+0"= All methods allowed (GET, POST, PUT, DELETE, etc.)"+1"= GET only"+2"= POST only- Custom combinations can be defined
Permission Validation Middleware
The authorize middleware validates that the authenticated user has permission to access the requested route:
const authenticate = (req, res, next) => {
return req.app.locals.roditClient.authenticate(req, res, next);
};
const authorize = (req, res, next) => {
return req.app.locals.roditClient.authorize(req, res, next);
};
// Apply both authentication and authorization
app.use('/api/admin', authenticate, authorize, adminRoutes);
// CRUDA endpoints with full protection
app.use('/api/cruda', authenticate, authorize, crudaRoutes);Permission Enforcement
// Example: CRUDA routes with permission checking
const router = express.Router();
// All routes require authentication + authorization
router.post('/create', async (req, res) => {
// User must have permission for POST /api/cruda/create
const { comment, author } = req.body;
// Create record in database
const result = await db.run(
'INSERT INTO comments (comment, author) VALUES (?, ?)',
[comment, author || req.user.roditId]
);
res.json({ id: result.lastID, requestId: req.requestId });
});
router.post('/list', async (req, res) => {
// User must have permission for POST /api/cruda/list
const records = await db.all('SELECT * FROM comments ORDER BY created_at DESC');
res.json({ records, requestId: req.requestId });
});
module.exports = router;Dynamic Permission Checking
// Check permissions programmatically
const client = req.app.locals.roditClient;
const hasPermission = client.isOperationPermitted('POST', '/api/admin/users');
if (!hasPermission) {
return res.status(403).json({
error: 'Forbidden',
message: 'You do not have permission to access this resource',
requestId: req.requestId
});
}
// Proceed with operationPermission Validation in Client Token Minting
When minting client tokens via /api/signclient, the server validates that requested permissions are a subset of the server's own permissions:
// Client requests these permissions:
const requestedPermissions = {
"/": "+0",
"/api/echo": "+0",
"/api/cruda/create": "+0"
};
// Server validates against its own permissioned_routes
// If any requested route is not in server's config, request is rejected with HTTP 400Session Management
Overview
The SDK includes a comprehensive session management system that:
- Tracks active user sessions
- Validates JWT tokens against session state
- Supports pluggable storage backends
- Automatically cleans up expired sessions
- Integrates with performance metrics
Session Storage Backends
1. In-Memory Storage (Default)
No configuration needed - works out of the box:
const client = await RoditClient.create('server');
// Uses InMemorySessionStorage by defaultPros: Fast, zero configuration
Cons: Sessions lost on server restart, not suitable for multi-server deployments
2. SQLite Storage (Recommended for Production)
Persistent storage using SQLite database:
const express = require('express');
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
const { RoditClient, setExpressSessionStore } = require('@rodit/rodit-auth-be');
// Configure BEFORE initializing RoditClient
const sessionStore = new SQLiteStore({
db: 'sessions.db',
dir: './data',
table: 'sessions'
});
setExpressSessionStore(sessionStore);
// Now initialize client
const client = await RoditClient.create('server');Pros: Persistent across restarts, simple setup, uses existing database infrastructure
Cons: Not suitable for multi-server deployments
3. Redis Storage (For Multi-Server)
npm install express-session connect-redis redisconst session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const { setExpressSessionStore } = require('@rodit/rodit-auth-be');
// Create Redis client
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://127.0.0.1:6379'
});
await redisClient.connect();
// Create Redis store
const redisStore = new RedisStore({
client: redisClient,
prefix: 'rodit:sess:',
ttl: 86400 // 24 hours
});
setExpressSessionStore(redisStore);
const client = await RoditClient.create('server');Pros: Shared sessions across multiple servers, high performance
Cons: Requires Redis infrastructure
Session Operations
// Get session manager
const sessionManager = roditClient.getSessionManager();
// Get active session count
const activeCount = await sessionManager.getActiveSessionCount();
// Enumerate sessions via storage
const allSessions = await sessionManager.storage.getAll();
// Or fallback using keys() + get()
const sessionIds = await sessionManager.storage.keys();
const sessions = [];
for (const id of sessionIds) {
const session = await sessionManager.storage.get(id);
if (session) sessions.push(session);
}
// Check if token is invalidated
const isInvalidated = await sessionManager.isTokenInvalidated(jwtToken);
// Manually close a session
await sessionManager.closeSession(sessionId);
// Run manual cleanup (removes expired sessions)
const cleanup = await sessionManager.runManualCleanup();
console.log(`Removed ${cleanup.removedSessionsCount} expired sessions`);Session Lifecycle
- Login - Session created, JWT token issued with session ID
- Active - Token validated on each request, session last_accessed updated
- Logout - Session closed, token invalidated, termination token issued
- Expiration - Sessions automatically expire based on JWT duration
- Cleanup - Expired sessions removed by automatic cleanup process
Token Invalidation
The SDK validates tokens by checking session state:
// Authentication middleware checks:
// 1. JWT signature validity
// 2. JWT expiration
// 3. Session exists and is active
// 4. Session not expired
// After logout, tokens are invalidated because:
// - Session status set to 'closed'
// - Subsequent requests fail authenticationConfiguration
Automatic Configuration Loading
The SDK automatically configures itself from multiple sources (in priority order):
- Environment Variables (Highest priority)
- Configuration Files (config/default.json, config/production.json)
- Vault Credentials (Production)
- SDK Defaults (Fallback)
Environment Configuration: NODE_ENV and LOG_LEVEL
The SDK uses two separate environment variables for configuration, following Node.js ecosystem standards:
NODE_ENV - Environment Type & Security Behavior
Controls environment-specific behavior and security settings:
Values:
production- Production environment (strict security, no error details)development- Development environment (relaxed security, detailed errors)test- Testing environment (allows bypasses for automated testing)staging- Staging environment (production security with optional verbose logging)
Default: production (secure by default)
Controls:
- ✅ Error detail exposure in API responses
- ✅ Peer public key requirement enforcement
- ✅ Webhook verification bypass (test mode only)
- ✅ Security-critical behavior
LOG_LEVEL - Logging Verbosity
Controls Winston logger verbosity independently from environment:
Values:
error- Only errorswarn- Warnings and errorsinfo- Informational messages, warnings, and errors (recommended for production)debug- Detailed debugging informationtrace- Maximum verbosity with full traces
Default: info
Controls:
- ✅ Winston logger output level
- ✅ Debug payload logging
- ✅ Log verbosity only (not security)
Separation of Concerns
// Environment detection (security)
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = process.env.NODE_ENV === 'development';
const isTest = process.env.NODE_ENV === 'test';
// Logging verbosity (independent)
const config = roditClient.getConfig();
const logLevel = config.get('LOG_LEVEL', 'info');Configuration Examples
Production (normal):
export NODE_ENV=production
export LOG_LEVEL=info
# Results in:
# - Strict security enforcement
# - No error details in responses
# - Minimal logging outputProduction (troubleshooting):
export NODE_ENV=production
export LOG_LEVEL=debug
# Results in:
# - Strict security enforcement (still production)
# - No error details in responses (still secure)
# - Verbose logging for debuggingDevelopment:
export NODE_ENV=development
export LOG_LEVEL=debug
# Results in:
# - Relaxed security for development
# - Detailed error messages in responses
# - Verbose loggingTesting:
export NODE_ENV=test
export LOG_LEVEL=error
# Results in:
# - Test mode (allows bypasses)
# - Detailed error messages
# - Only errors logged (cleaner test output)Staging:
export NODE_ENV=production
export LOG_LEVEL=warn
# Results in:
# - Production security
# - No error details exposed
# - Only warnings and errors loggedBehavior Matrix
| Scenario | NODE_ENV | LOG_LEVEL | Security | Error Details | Logging |
|---|---|---|---|---|---|
| Production | production |
info |
✅ Strict | ❌ Hidden | Minimal |
| Production Debug | production |
debug |
✅ Strict | ❌ Hidden | Verbose |
| Development | development |
debug |
⚠️ Relaxed | ✅ Shown | Verbose |
| Testing | test |
error |
⚠️ Bypass OK | ✅ Shown | Errors only |
| Staging | production |
warn |
✅ Strict | ❌ Hidden | Warnings |
Vault-Based Configuration (Production)
For production deployments, credentials are loaded from HashiCorp Vault:
# Environment variables for vault
export RODIT_NEAR_CREDENTIALS_SOURCE=vault
export VAULT_ENDPOINT=https://vault.example.com
export VAULT_ROLE_ID=your-role-id
export VAULT_SECRET_ID=your-secret-id
export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
export SERVICE_NAME=your-service-name
export NEAR_CONTRACT_ID=your-contract.testnetFile-Based Configuration (Development)
For development, credentials can be loaded from files:
export RODIT_NEAR_CREDENTIALS_SOURCE=file
export CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.jsonAccessing Configuration
// Get complete RODiT configuration
const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;
// Access RODiT token metadata
const jwtDuration = metadata.jwt_duration; // JWT expiration time
const maxRequests = metadata.max_requests; // Rate limit
const maxRqWindow = metadata.maxrq_window; // Rate limit window
const apiEndpoint = metadata.subjectuniqueidentifier_url; // API URL
const webhookUrl = metadata.webhook_url; // Webhook endpoint
// Parse permissioned routes
const permissionedRoutes = JSON.parse(metadata.permissioned_routes || '{}');
// Use SDK config for application settings
const config = roditClient.getConfig();
const serverPort = config.get('SERVERPORT', 3000);
const logLevel = config.get('LOG_LEVEL', 'info');
const dbPath = config.get('API_DEFAULT_OPTIONS.DB_PATH');Dynamic Rate Limiting
// Configure rate limiting from RODiT token
const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;
if (metadata.max_requests && metadata.maxrq_window) {
const maxRequests = parseInt(metadata.max_requests);
const windowSeconds = parseInt(metadata.maxrq_window);
const rateLimiter = roditClient.getRateLimitMiddleware();
app.use(rateLimiter(maxRequests, windowSeconds));
}Logging & Monitoring
Structured Logging
The SDK provides comprehensive structured logging:
const { logger } = require('@rodit/rodit-auth-be');
// Basic logging
logger.info('Operation completed', {
component: 'UserService',
operation: 'createUser',
userId: '123',
duration: 150
});
// Context-aware logging
logger.infoWithContext('Request processed', {
component: 'API',
method: 'POST',
path: '/api/users',
requestId: req.requestId,
userId: req.user?.id,
duration: Date.now() - req.startTime
});
// Error logging with metrics
logger.errorWithContext('Operation failed', {
component: 'UserService',
operation: 'createUser',
requestId: req.requestId,
error: error.message,
stack: error.stack
}, error);Loki with the SDK (canonical)
Use this as the authoritative guide for configuring logging with the SDK.
Environment variables
export LOKI_URL=https://<your-loki-host>:3100
export LOKI_BASIC_AUTH="username:password" # store in secrets
export LOKI_TLS_SKIP_VERIFY=true # only for self-signed/test
export LOG_LEVEL=info
export SERVICE_NAME=clienttestapi-apiThese are already mapped in config/custom-environment-variables.json, so container/CI env vars will flow into the app.
How the SDK selects/configures the logger
- Default: JSON to stdout only (no Loki). Honors
LOG_LEVEL, addsservice_name. - Production: Create a Winston logger with a
winston-lokitransport and inject it once:logger.setLogger(customLogger). - Access:
const { logger } = require('@rodit/rodit-auth-be')orroditClient.getLogger()both delegate to the same facade.
Direct-to-Loki via winston-loki (recommended)
const { logger } = require('@rodit/rodit-auth-be');
const winston = require('winston');
const LokiTransport = require('winston-loki');
const transports = [new winston.transports.Console({ format: winston.format.json() })];
if (process.env.LOKI_URL) {
const lokiOptions = {
host: process.env.LOKI_URL,
basicAuth: process.env.LOKI_BASIC_AUTH, // Basic Auth for Loki
labels: { app: process.env.SERVICE_NAME || 'clienttestapi-api', component: 'rodit-sdk' },
json: true,
batching: true
};
if ((process.env.LOKI_TLS_SKIP_VERIFY || '').toLowerCase() === 'true') {
lokiOptions.ssl = { rejectUnauthorized: false };
}
transports.push(new LokiTransport(lokiOptions));
}
const customLogger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports
});
logger.setLogger(customLogger);Promtail (optional alternative)
- Only needed if you must ship file logs (e.g., Nginx) or cannot push directly from the process.
- SDK logs do not need Promtail when using
winston-loki. - If you keep Promtail, ensure the scrape path matches files (see
promtail/promtail-config.yml).
CI/CD notes
.github/workflows/deploy.ymlpassesLOKI_URL,LOKI_TLS_SKIP_VERIFY,LOKI_BASIC_AUTHinto the container;src/app.jsconfig injects the transport at startup.- Promtail steps are commented out. If you don’t need file-based ingestion, you can remove Promtail steps and the
promtail/directory entirely. Keep it only for Nginx/file logs. - Store
LOKI_BASIC_AUTHin CI/CD secrets; never commit credentials.
Quick verification
- Start the app with
LOKI_URLandLOKI_BASIC_AUTHset. - Emit a test log:
logger.info('Loki test', { component: 'SmokeTest' }). - In Grafana Explore, query with
{app="clienttestapi-api"}and confirm logs.
Performance Tuning
Session Storage Optimization
Production Recommendation: Use persistent storage (SQLite or Redis) instead of in-memory storage.
// ✅ Best for single-server deployments
const SQLiteStore = require('connect-sqlite3')(require('express-session'));
const sessionStore = new SQLiteStore({
db: 'sessions.db',
dir: './data',
table: 'sessions',
// Optimize for performance
concurrentDB: true
});
setExpressSessionStore(sessionStore);
// ✅ Best for multi-server deployments
const RedisStore = require('connect-redis').default;
const redisClient = createClient({
url: process.env.REDIS_URL,
// Connection pooling
socket: {
keepAlive: true,
reconnectStrategy: (retries) => Math.min(retries * 50, 500)
}
});
await redisClient.connect();
const redisStore = new RedisStore({
client: redisClient,
prefix: 'rodit:sess:',
ttl: 86400,
// Disable touch for better performance if sessions don't need frequent updates
disableTouch: false
});
setExpressSessionStore(redisStore);Performance Impact:
- In-memory: Fastest but sessions lost on restart
- SQLite: Good performance, persistent, suitable for single server
- Redis: Best for distributed systems, shared sessions across servers
Logging Optimization
Production Settings:
// Use appropriate log level
export LOG_LEVEL=info # Avoid 'debug' or 'trace' in production
// Batch Loki logs for better performance
const lokiOptions = {
host: process.env.LOKI_URL,
basicAuth: process.env.LOKI_BASIC_AUTH,
labels: { app: 'your-app', component: 'rodit-sdk' },
json: true,
batching: true, // ✅ Enable batching
interval: 5, // Send every 5 seconds
timeout: 30000, // 30 second timeout
gracefulShutdown: true // Flush logs on shutdown
};Log Level Impact:
error: Minimal overhead, production defaultwarn: Low overhead, recommended for productioninfo: Moderate overhead, good for production monitoringdebug: High overhead, development onlytrace: Very high overhead, debugging only
Rate Limiting Configuration
Configure rate limiting based on your RODiT token metadata:
const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;
// Use RODiT token rate limits
const maxRequests = parseInt(metadata.max_requests);
const windowSeconds = parseInt(metadata.maxrq_window);
const rateLimiter = roditClient.getRateLimitMiddleware();
app.use(rateLimiter(maxRequests, windowSeconds));
// Example: 100 requests per 15 minutes
// maxRequests = 100
// windowSeconds = 900Tuning Tips:
- Set
max_requestsbased on expected traffic patterns - Use shorter
maxrq_windowfor burst protection - Monitor rate limit hits via performance metrics
JWT Token Duration
Optimize JWT duration for security vs. performance trade-off:
// In your RODiT token metadata:
{
"jwt_duration": 3600 // 1 hour (default)
}Recommendations:
- Short duration (900-1800s): High security, more token refreshes
- Medium duration (3600-7200s): Balanced, recommended for most applications
- Long duration (>7200s): Fewer refreshes but lower security
Impact:
- Shorter durations = More authentication overhead
- Longer durations = Less overhead but longer exposure if token compromised
Database Connection Pooling
For CRUDA operations with SQLite:
const sqlite3 = require('sqlite3');
const { open } = require('sqlite');
const db = await open({
filename: '/app/data/database.sqlite',
driver: sqlite3.Database,
// Enable WAL mode for better concurrent access
mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE
});
// Enable WAL mode for better performance
await db.run('PRAGMA journal_mode = WAL');
await db.run('PRAGMA synchronous = NORMAL');
await db.run('PRAGMA cache_size = 10000');
await db.run('PRAGMA temp_store = MEMORY');Request Context Middleware Optimization
Minimize middleware overhead:
// ✅ Efficient request context
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || ulid();
req.startTime = Date.now();
next();
});
// ❌ Avoid expensive operations in every request
app.use((req, res, next) => {
// Don't do heavy computation here
// Don't make external API calls here
next();
});Performance Metrics Collection
Use the performance service efficiently:
const performanceService = roditClient.getPerformanceService();
// Record metrics selectively
if (duration > 1000) { // Only log slow requests
performanceService.recordMetric('slow_request', duration, {
method: req.method,
path: req.path
});
}
// Batch metric collection
const metrics = {
request_count: 1,
duration_ms: duration,
status_code: res.statusCode
};
performanceService.recordBatch(metrics);Memory Management
Session Cleanup:
// Automatic cleanup runs every 15 minutes by default
// Adjust if needed for your use case
const sessionManager = roditClient.getSessionManager();
// Manual cleanup for immediate memory reclamation
await sessionManager.runManualCleanup();
// Monitor session count
const activeCount = await sessionManager.getActiveSessionCount();
if (activeCount > 10000) {
logger.warn('High session count detected', { activeCount });
}Caching Strategies
Cache frequently accessed data:
// Cache RODiT configuration (changes rarely)
let cachedConfig = null;
let configCacheTime = 0;
const CONFIG_CACHE_TTL = 300000; // 5 minutes
async function getConfig() {
const now = Date.now();
if (cachedConfig && (now - configCacheTime) < CONFIG_CACHE_TTL) {
return cachedConfig;
}
cachedConfig = await roditClient.getConfigOwnRodit();
configCacheTime = now;
return cachedConfig;
}Monitoring Recommendations
Key Metrics to Track:
- Request duration (p50, p95, p99)
- Authentication success/failure rate
- Session count and growth rate
- Rate limit hits
- Error rates by endpoint
- Memory usage and session storage size
Grafana Dashboard Queries:
# Request duration
histogram_quantile(0.95, rate(request_duration_ms_bucket[5m]))
# Authentication failures
rate(authentication_errors_total[5m])
# Active sessions
rodit_active_sessions
# Rate limit hits
rate(rate_limit_exceeded_total[5m])Webhooks
Overview
The SDK supports sending webhooks for important events. Webhook URLs are configured in the RODiT token metadata. Webhooks are configured in your RODiT token:
{
"webhook_url": "https://webhook.example.com:3444",
"webhook_cidr": "0.0.0.0/0"
}Sending Webhooks
// Get webhook handler from client
const roditClient = req.app.locals.roditClient;
// Send webhook for an event
const webhookPayload = {
event: 'comment_created',
data: {
id: comment.id,
author: comment.author,
timestamp: new Date().toISOString()
},
isError: false
};
try {
const result = await roditClient.send_webhook(webhookPayload, req);
if (result.success) {
logger.info('Webhook sent successfully', {
component: 'CRUDA',
event: webhookPayload.event,
requestId: req.requestId
});
}
} catch (error) {
// Webhook failures don't crash the application
logger.warn('Webhook delivery failed', {
component: 'CRUDA',
event: webhookPayload.event,
error: error.message,
requestId: req.requestId
});
}Webhook Error Handling
// Graceful webhook handling in CRUDA operations
const logAndSendWebhook = async (payload, req = null) => {
try {
const roditClient = req?.app?.locals?.roditClient;
if (!roditClient) {
logger.warn('RoditClient not available, skipping webhook', {
component: 'CRUDA',
event: payload?.event
});
return { success: false, error: 'RoditClient not available' };
}
return await roditClient.send_webhook(payload, req);
} catch (error) {
// Log but don't throw - webhook failures shouldn't crash the app
logger.error('Webhook delivery failed', {
component: 'CRUDA',
event: payload?.event,
error: error.message
});
return { success: false, error: error.message };
}
};Advanced Usage
Route Module Pattern
Create reusable route modules that access the shared RoditClient:
// routes/protected.js
const express = require('express');
const { logger } = require('@rodit/rodit-auth-be');
const router = express.Router();
// Middleware that uses the shared client
const authenticate = (req, res, next) => {
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({ error: 'Authentication service unavailable' });
}
return client.authenticate(req, res, next);
};
const authorize = (req, res, next) => {
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({ error: 'Authentication service unavailable' });
}
return client.authorize(req, res, next);
};
// Protected route with full authentication and authorization
router.get('/data', authenticate, authorize, async (req, res) => {
const startTime = Date.now();
try {
// Your business logic here
const data = await processUserData(req.user.id);
logger.infoWithContext('Data retrieved successfully', {
component: 'ProtectedRoutes',
method: 'getData',
userId: req.user.id,
requestId: req.requestId,
duration: Date.now() - startTime
});
res.json({ data, requestId: req.requestId });
} catch (error) {
logger.errorWithContext('Failed to retrieve data', {
component: 'ProtectedRoutes',
method: 'getData',
userId: req.user.id,
requestId: req.requestId,
duration: Date.now() - startTime,
error: error.message
}, error);
res.status(500).json({
error: 'Internal server error',
requestId: req.requestId
});
}
});
module.exports = router;Portal Authentication (Server-to-Server)
For server-to-server authentication (e.g., minting client tokens):
// routes/signclient.js
const router = express.Router();
router.post('/signclient', authenticate, authorize, async (req, res) => {
const { tobesignedValues, mintingfee, mintingfeeaccount } = req.body;
const client = req.app.locals.roditClient;
const logger = client.getLogger();
try {
// Validate requested permissions against server's permissions
const configObject = await client.getConfigOwnRodit();
const serverPermissions = JSON.parse(
configObject.own_rodit.metadata.permissioned_routes || '{}'
);
const requestedPermissions = JSON.parse(
tobesignedValues.permissioned_routes || '{}'
);
// Validate that all requested routes exist in server config
// (Implementation details in actual code)
// Authenticate to portal and mint client token
const port = configObject.port || 8443;
const result = await client.login_portal(configObject, port);
if (result.error) {
return res.status(500).json({
error: 'Portal authentication failed',
details: result.message,
requestId: req.requestId
});
}
// Sign the client token via portal
const signedToken = await signPortalRodit(
port,
tobesignedValues,
mintingfee,
mintingfeeaccount,
client
);
res.json({
signedToken,
requestId: req.requestId
});
} catch (error) {
logger.errorWithContext('Client token minting failed', {
component: 'SignClient',
requestId: req.requestId,
error: error.message
}, error);
res.status(500).json({
error: 'Token minting failed',
requestId: req.requestId
});
}
});
module.exports = router;SignPortal URL Configuration
Overview
When performing server-to-server authentication with SignPortal (e.g., minting client tokens), the SDK automatically constructs the SignPortal URL from the serviceprovider_id field in your RODiT token metadata.
Smart Contract Name Format
The SignPortal URL is derived from the smart contract component (sc=) in your serviceprovider_id. The SDK supports two formats:
Standard Format (3+ components):
sc=<number>-<domain>-<tld>.nearExample:
serviceprovider_id: "bc=near.org;sc=10975-discernible-org.near;id=..."Parsing:
- Split by
.:["10975-discernible-org", "near"] - Take first part:
10975-discernible-org - Split by
-:["10975", "discernible", "org"] - Extract domain:
discernible(index 1) - Extract TLD:
org(index 2) - Result:
https://signportal.discernible.org:8443
Alternative Format (2 components):
sc=<domain>-<tld>.nearExample:
serviceprovider_id: "bc=near.org;sc=roditcorp-com.near;id=..."Parsing:
- Split by
.:["roditcorp-com", "near"] - Take first part:
roditcorp-com - Split by
-:["roditcorp", "com"] - Extract domain:
roditcorp(index 0) - Extract TLD:
com(index 1) - Result:
https://signportal.roditcorp.com:8443
serviceprovider_id Structure
The complete serviceprovider_id format:
bc=<blockchain>;sc=<smart-contract>;id=<identifier>[;id=<additional-id>]Components:
bc=- Blockchain identifier (e.g.,near.org)sc=- Smart contract name (used to construct SignPortal URL)id=- One or more identifier components
Example:
{
"serviceprovider_id": "bc=near.org;sc=roditcorp-com.near;id=01K8QECHMKFVNWQ54PJ2W2GMA7;id=01K8QECHMM1214VMDHSH7JM6H8"
}URL Construction Method
The SDK uses roditClient.getPortalUrl(serviceProviderId, port) to construct the SignPortal URL:
const client = req.app.locals.roditClient;
const configObject = await client.getConfigOwnRodit();
const serviceProviderId = configObject.own_rodit.metadata.serviceprovider_id;
const portalPort = 8443;
// Automatically constructs: https://signportal.<domain>.<tld>:8443
const portalUrl = client.getPortalUrl(serviceProviderId, portalPort);Troubleshooting
Error: "Failed to parse URL from " (empty string)
- Cause:
serviceprovider_idis empty or undefined in your RODiT configuration - Solution: Verify your RODiT token has a valid
serviceprovider_idfield - Check: Run
./infra/roditwallet.sh <private-key> <token-id>to view token metadata
Error: "Invalid serviceprovider_id format: missing sc= component"
- Cause: The
serviceprovider_iddoesn't contain ansc=component - Solution: Ensure your token includes the smart contract identifier
- Format:
bc=near.org;sc=<contract-name>.near;id=...
Error: "Invalid domain format in smart contract"
- Cause: Smart contract name has fewer than 2 components when split by
- - Solution: Use format
<domain>-<tld>or<number>-<domain>-<tld> - Valid:
roditcorp-com.near,10975-discernible-org.near - Invalid:
roditcorp.near,mycontract.near
Configuration Verification
To verify your SignPortal URL configuration:
const client = req.app.locals.roditClient;
const logger = client.getLogger();
try {
const configObject = await client.getConfigOwnRodit();
const serviceProviderId = configObject.own_rodit.metadata.serviceprovider_id;
logger.info('RODiT Configuration', {
component: 'SignPortal',
serviceProviderId,
hasServiceProviderId: !!serviceProviderId
});
if (serviceProviderId) {
const portalUrl = client.getPortalUrl(serviceProviderId, 8443);
logger.info('SignPortal URL constructed', {
component: 'SignPortal',
portalUrl
});
}
} catch (error) {
logger.error('SignPortal URL construction failed', {
component: 'SignPortal',
error: error.message
});
}CRUDA Operations Example
Complete CRUD implementation with authentication, authorization, webhooks, and performance tracking:
// protected/cruda.js
const express = require('express');
const router = express.Router();
const { RoditClient } = require('@rodit/rodit-auth-be');
const sqlite3 = require('sqlite3');
const { open } = require('sqlite');
const { ulid } = require('ulid');
const sdkClient = new RoditClient();
const logger = sdkClient.getLogger();
let db;
// Initialize database
const initializeDatabase = async () => {
db = await open({
filename: '/app/data/database.sqlite',
driver: sqlite3.Database
});
await db.run(`CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
comment TEXT NOT NULL,
author TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
};
// Webhook helper
const logAndSendWebhook = async (payload, req) => {
try {
const roditClient = req?.app?.locals?.roditClient;
if (!roditClient) return { success: false };
return await roditClient.send_webhook(payload, req);
} catch (error) {
logger.error('Webhook failed', { error: error.message });
return { success: false, error: error.message };
}
};
// CREATE
router.post('/create', async (req, res) => {
const { comment, author } = req.body;
const requestId = req.requestId || ulid();
try {
const result = await db.run(
'INSERT INTO comments (comment, author) VALUES (?, ?)',
[comment, author || req.user.roditId]
);
// Send webhook
await logAndSendWebhook({
event: 'comment_created',
data: { id: result.lastID, comment, author },
isError: false
}, req);
res.json({ id: result.lastID, requestId });
} catch (error) {
logger.errorWithContext('Create failed', {
component: 'CRUDA',
error: error.message,
requestId
}, error);
res.status(500).json({ error: 'Create failed', requestId });
}
});
// LIST
router.post('/list', async (req, res) => {
try {
const records = await db.all(
'SELECT * FROM comments ORDER BY created_at DESC'
);
res.json({ records, requestId: req.requestId });
} catch (error) {
res.status(500).json({ error: 'List failed', requestId: req.requestId });
}
});
// READ
router.post('/read', async (req, res) => {
const { id } = req.body;
try {
const record = await db.get('SELECT * FROM comments WHERE id = ?', [id]);
if (!record) {
return res.status(404).json({ error: 'Not found', requestId: req.requestId });
}
res.json({ record, requestId: req.requestId });
} catch (error) {
res.status(500).json({ error: 'Read failed', requestId: req.requestId });
}
});
// UPDATE
router.post('/update', async (req, res) => {
const { id, comment } = req.body;
try {
await db.run(
'UPDATE comments SET comment = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[comment, id]
);
await logAndSendWebhook({
event: 'comment_updated',
data: { id, comment },
isError: false
}, req);
res.json({ success: true, requestId: req.requestId });
} catch (error) {
res.status(500).json({ error: 'Update failed', requestId: req.requestId });
}
});
// DELETE
router.post('/destroy', async (req, res) => {
const { id } = req.body;
try {
await db.run('DELETE FROM comments WHERE id = ?', [id]);
await logAndSendWebhook({
event: 'comment_deleted',
data: { id },
isError: false
}, req);
res.json({ success: true, requestId: req.requestId });
} catch (error) {
res.status(500).json({ error: 'Delete failed', requestId: req.requestId });
}
});
// Export initialization function
module.exports = router;
module.exports.initializeDatabase = initializeDatabase;API Reference
RoditClient Class
The main client class for all RODiT operations.
Static Methods
RoditClient.create(role)
Create and initialize a RODiT client in one step.
const client = await RoditClient.create('server'); // For server applications
const client = await RoditClient.create('client'); // For client applications
const client = await RoditClient.create('portal'); // For portal authenticationParameters:
role(string): Client role -'server','client', or'portal'
Returns: Promise<RoditClient> - Fully initialized client instance
Throws: Error if initialization fails (e.g., missing credentials, Vault connection failure)
RoditClient.createTestInstance()
Create an independent test instance with isolated state for testing purposes.
const testClient1 = await RoditClient.createTestInstance();
const testClient2 = await RoditClient.createTestInstance();
// Each client has independent stateManager for concurrent testing
console.log(testClient1.stateManager.instanceId !== testClient2.stateManager.instanceId); // trueReturns: Promise<RoditClient> - Test client instance with testMode: true
Use Case: Testing concurrent sessions, session isolation, or multiple client scenarios
Instance Methods
authenticate(req, res, next)
Express middleware for authenticating API requests. Validates JWT tokens and populates req.user.
const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
app.use('/api/protected', authenticate, handler);Validates:
- JWT signature
- JWT expiration
- Session exists and is active
- Token not invalidated
Populates: req.user with decoded JWT claims
authorize(req, res, next)
Express middleware for validating route permissions. Must be used after authenticate.
const authorize = (req, res, next) => roditClient.authorize(req, res, next);
app.use('/api/admin', authenticate, authorize, handler);Validates: User has permission for the requested route and HTTP method
login_client(req, res)
Handle Express login requests from clients. Validates RODiT credentials and issues JWT token.
app.post('/api/login', (req, res) => roditClient.login_client(req, res));Request Body:
{
roditid: string,
timestamp: number,
roditid_base64url_signature: string
}Response:
{
message: 'Login successful',
token: 'eyJhbGci...',
requestId: '01HQXYZ...'
}logout_client(req, res)
Handle Express logout requests. Closes session and invalidates JWT token.
app.post('/api/logout', authenticate, (req, res) => {
return roditClient.logout_client(req, res);
});Response:
{
message: 'Logout successful',
terminationToken: 'eyJhbGci...', // Short-lived token
requestId: '01HQXYZ...'
}login_portal(configObject, port)
Authenticate to RODiT portal for server-to-server operations.
const configObject = await roditClient.getConfigOwnRodit();
const result = await roditClient.login_portal(configObject, 8443);Returns: Promise<Object> - Portal authentication result
login_server(options)
Authenticate this server to another RODiT server.
const result = await roditClient.login_server({
serverUrl: 'https://api.example.com',
credentials: {...}
});Returns: Promise<Object> - Authentication result with token
logout_server()
Logout from server-to-server session.
const result = await roditClient.logout_server();Returns: Promise<Object> - Logout result with session closure status
getConfigOwnRodit()
Get the complete RODiT configuration including token metadata.
const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;
const tokenId = configObject.own_rodit.token_id;Returns: Promise<Object> - Complete RODiT configuration
Structure:
{
own_rodit: {
token_id: string,
metadata: {
jwt_duration: number,
max_requests: string,
maxrq_window: string,
permissioned_routes: string, // JSON string
subjectuniqueidentifier_url: string,
webhook_url: string,
// ... other metadata fields
}
},
port: number
}isOperationPermitted(method, path)
Check if an operation is permitted based on token permissions.
const hasPermission = roditClient.isOperationPermitted('POST', '/api/admin/users');
if (!hasPermission) {
return res.status(403).json({ error: 'Forbidden' });
}Parameters:
method(string): HTTP methodpath(string): API path
Returns: boolean
getStateManager()
Get the authentication state manager.
const stateManager = roditClient.getStateManager();Returns: AuthStateManager instance
getRoditManager()
Get the RODiT manager for credential operations.
const roditManager = roditClient.getRoditManager();
const credentials = await roditManager.getCredentials('server');Returns: RoditManager instance
getSessionManager()
Get the session manager.
const sessionManager = roditClient.getSessionManager();
const activeCount = await sessionManager.getActiveSessionCount();Returns: SessionManager instance
getLogger()
Get the logger instance.
const logger = roditClient.getLogger();
logger.info('Message', { component: 'MyComponent' });Returns: Logger instance
getLoggingMiddleware()
Get the logging middleware.
const loggingmw = roditClient.getLoggingMiddleware();
app.use(loggingmw);Returns: Express middleware function
getRateLimitMiddleware()
Get the rate limiting middleware factory.
const ratelimitmw = roditClient.getRateLimitMiddleware();
const limiter = ratelimitmw(100, 900); // 100 requests per 15 minutes
app.use(limiter);Parameters:
maxRequests(number): Maximum requests allowedwindowSeconds(number): Time window in seconds
Returns: Express middleware function
getPerformanceService()
Get the performance tracking service.
const performanceService = roditClient.getPerformanceService();
performanceService.recordRequest(req);
performanceService.recordMetric('operation_duration', 150, { operation: 'db_query' });Returns: PerformanceService instance
getConfig()
Get the configuration service.
const config = roditClient.getConfig();
const port = config.get('SERVERPORT', 3000);
const dbPath = config.get('API_DEFAULT_OPTIONS.DB_PATH');Returns: Config instance
getWebhookHandler()
Get the webhook handler.
const webhookHandler = roditClient.getWebhookHandler();Returns: WebhookHandler instance
send_webhook(payload, req)
Send a webhook notification.
const result = await roditClient.send_webhook({
event: 'user_action',
data: { userId: '123', action: 'login' },
isError: false
}, req);Parameters:
payload(Object): Webhook payloadevent(string): Event namedata(Object): Event dataisError(boolean): Whether this is an error event
req(Object): Express request object (optional)
Returns: Promise<Object> - { success: boolean, ... }
Exported Components
The SDK exports these components for direct use:
const {
RoditClient, // Main client class
logger, // Logger instance
stateManager, // Authentication state manager
roditManager, // RODiT credential manager
sessionManager, // Session manager
setExpressSessionStore, // Configure session storage
configureStorageFromConfig, // Auto-configure storage
createExpressSessionMiddleware, // Create session middleware
InMemorySessionStorage, // Default storage class
SessionManager, // SessionManager facade
blockchainService, // Blockchain operations
utils, // Utility functions
config, // Configuration service
performanceService, // Performance tracking
authenticate_apicall, // Authentication middleware
login_client, // Login handler
logout_client, // Logout handler
login_client_withnep413, // NEP-413 login
login_portal, // Portal authentication
login_server, // Server authentication
logout_server, // Server logout
validate_jwt_token_be, // JWT validation
generate_jwt_token, // JWT generation
validatepermissions, // Permission middleware
webhookHandler, // Webhook handler
versioningMiddleware, // API versioning
loggingmw, // Logging middleware
ratelimitmw, // Rate limiting middleware
versionManager, // Version manager
VersionManager // Version manager class
} = require('@rodit/rodit-auth-be');RODiT Token Metadata Fields
When you call roditClient.getConfigOwnRodit(), you get access to these metadata fields:
| Field | Type | Description |
|---|---|---|
token_id |
string | Unique RODiT token identifier |
allowed_cidr |
string | Permitted IP address ranges (CIDR format) |
allowed_iso3166list |
string | Geographic restrictions (JSON string) |
jwt_duration |
number | JWT token lifetime in seconds |
max_requests |
string | Rate limit - maximum requests per window |
maxrq_window |
string | Rate limit - time window in seconds |
not_before |
string | Token validity start date (ISO format) |
not_after |
string | Token validity end date (ISO format) |
openapijson_url |
string | OpenAPI specification URL |
permissioned_routes |
string | Allowed API routes and methods (JSON string) |
serviceprovider_id |
string | Blockchain contract and service provider info |
serviceprovider_signature |
string | Cryptographic signature for verification |
subjectuniqueidentifier_url |
string | Primary API service endpoint |
userselected_dn |
string | User-selected display name |
webhook_cidr |
string | Allowed IP ranges for webhooks |
webhook_url |
string | Webhook endpoint URL |
Best Practices
1. Single Client Initialization
Always initialize the RoditClient once in your main application file:
// ✅ Good - Single initialization
async function startServer() {
const roditClient = await RoditClient.create('server');
app.locals.roditClient = roditClient;
// Mount protected routes AFTER client initialization
const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
const authorize = (req, res, next) => roditClient.authorize(req, res, next);
app.use('/api/echo', authenticate, echoRoutes);
app.use('/api/cruda', authenticate, authorize, crudaRoutes);
// ... rest of server setup
}
// ❌ Bad - Multiple initializations
app.get('/route1', async (req, res) => {
const client = await RoditClient.create('server'); // Don't do this
});2. Use App.locals for Shared Access
Store the client in app.locals for access across all routes:
// ✅ Good - Shared instance via app.locals
const router = express.Router();
router.get('/data', (req, res) => {
const client = req.app.locals.roditClient;
const logger = client.getLogger();
logger.info('Processing request', {
component: 'DataRoute',
userId: req.user?.id,
requestId: req.requestId
});
res.json({ data: 'example' });
});
// ❌ Bad - Creating new instances in routes
const { RoditClient } = require('@rodit/rodit-auth-be');
const client = new RoditClient(); // Don't do this in routes3. Proper Error Handling
Always wrap SDK operations in try-catch blocks and include request context:
// ✅ Good - Comprehensive error handling
app.get('/api/data', authenticate, async (req, res) => {
const startTime = Date.now();
const client = req.app.locals.roditClient;
const logger = client.getLogger();
try {
const data = await processData(req.user.id);
logger.infoWithContext('Request successful', {
component: 'API',
method: 'getData',
userId: req.user.id,
requestId: req.requestId,
duration: Date.now() - startTime
});
res.json({ data, requestId: req.requestId });
} catch (error) {
logger.errorWithContext('Request failed', {
component: 'API',
method: 'getData',
userId: req.user.id,
requestId: req.requestId,
duration: Date.now() - startTime,
error: error.message
}, error);
res.status(500).json({
error: 'Internal server error',
requestId: req.requestId
});
}
});4. Structured Logging
Use consistent logging patterns with context:
// ✅ Good - Structured logging with context
const logger = req.app.locals.roditClient.getLogger();
logger.infoWithContext('User action completed', {
component: 'UserService',
action: 'updateProfile',
userId: user.id,
requestId: req.requestId,
duration: Date.now() - startTime,
changes: Object.keys(updates)
});
// For errors, pass the error object
logger.errorWithContext('Operation failed', {
component: 'UserService',
action: 'updateProfile',
userId: user.id,
requestId: req.requestId,
error: error.message
}, error);
// ❌ Bad - Unstructured logging
console.log('User updated profile'); // Don't do this5. Environment-Specific Configuration
Use environment variables for sensitive and environment-specific values:
// ✅ Good - Environment-aware configuration
const config = roditClient.getConfig();
const logLevel = config.get('LOG_LEVEL', 'info');
const isProduction = ['info', 'warn', 'error'].includes(logLevel);
// Production should use vault credentials
if (isProduction && process.env.RODIT_NEAR_CREDENTIALS_SOURCE !== 'vault') {
logger.warn('Production environment should use vault credentials', {
component: 'Configuration',
environment: 'production',
credentialsSource: process.env.RODIT_NEAR_CREDENTIALS_SOURCE || 'not-set'
});
}
// Configure session storage before initializing client
if (isProduction) {
const SQLiteStore = require('connect-sqlite3')(require('express-session'));
const sessionStore = new SQLiteStore({
db: 'sessions.db',
dir: config.get('API_DEFAULT_OPTIONS.DB_PATH', './data')
});
setExpressSessionStore(sessionStore);
}6. Graceful Shutdown
Implement proper shutdown handling:
// ✅ Good - Graceful shutdown
const shutdown = async (signal) => {
const logger = roditClient.getLogger();
logger.info('Shutting down gracefully', {
component: 'AppLifecycle',
signal: signal || 'unknown',
time: new Date().toISOString()
});
if (server) {
server.close(async () => {
logger.info('HTTP server closed');
// Close database connections
if (db && typeof db.close === 'function') {
await db.close();
logger.info('Database connections closed');
}
// Close session store
if (sessionStore && typeof sessionStore.close === 'function') {
await new Promise((resolve) => sessionStore.close(resolve));
logger.info('Session store closed');
}
process.exit(0);
});
// Force shutdown after timeout
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
}
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));7. Request Context and Performance Tracking
Always include request context and track performance:
// ✅ Good - Request context and performance tracking
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || ulid();
req.startTime = Date.now();
next();
});
// Performance monitoring
app.use((req, res, next) => {
const performanceService = roditClient.getPerformanceService();
if (performanceService) {
performanceService.recordRequest(req);
}
res.on('finish', () => {
const duration = Date.now() - req.startTime;
if (performanceService) {
performanceService.recordMetric('request_duration_ms', duration, {
method: req.method,
path: req.path,
status: res.statusCode
});
if (res.statusCode >= 400) {
performanceService.recordMetric('error_count', 1, {
method: req.method,
path: req.path,
status: res.statusCode
});
}
}
});
next();
});8. Login Endpoint Protection
CRITICAL: Never protect the login endpoint with authentication middleware:
// ✅ Good - Login endpoint without authentication
app.post('/api/login', (req, res) => {
req.logAction = 'login-attempt';
return roditClient.login_client(req, res);
});
// ❌ Bad - Login endpoint with authentication (creates circular dependency)
app.post('/api/login', authenticate, (req, res) => { // DON'T DO THIS
return roditClient.login_client(req, res);
});
// ✅ Good - Logout endpoint with authentication
app.post('/api/logout', authenticate, (req, res) => {
req.logAction = 'logout-attempt';
return roditClient.logout_client(req, res);
});9. Route Mounting Order
Mount protected routes AFTER client initialization:
// ✅ Good - Correct order
async function startServer() {
// 1. Configure session storage
setExpressSessionStore(sessionStore);
// 2. Initialize client
roditClient = await RoditClient.create('server');
app.locals.roditClient = roditClient;
// 3. Create middleware
const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
const authorize = (req, res, next) => roditClient.authorize(req, res, next);
// 4. Mount public routes
app.post('/api/login', loginRoute);
// 5. Mount protected routes
app.use('/api/echo', authenticate, echoRoutes);
app.use('/api/cruda', authenticate, authorize, crudaRoutes);
app.post('/api/logout', authenticate, logoutRoute);
// 6. Start server
app.listen(port);
}
// ❌ Bad - Routes mounted before client initialization
app.use('/api/echo', authenticate, echoRoutes); // authenticate is undefined!
roditClient = await RoditClient.create('server');Security Considerations
1. Credential Storage
Production: Always use HashiCorp Vault for credential storage.
# ✅ Production - Vault-based credentials
export RODIT_NEAR_CREDENTIALS_SOURCE=vault
export VAULT_ENDPOINT=https://vault.example.com
export VAULT_ROLE_ID=your-role-id
export VAULT_SECRET_ID=your-secret-id
# ❌ Never commit credentials to version control
# ❌ Never use file-based credentials in productionDevelopment: Use file-based credentials with proper permissions.
# Development only
export RODIT_NEAR_CREDENTIALS_SOURCE=file
export CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.json
# Set restrictive file permissions
chmod 600 ./credentials/rodit-credentials.json2. JWT Token Security
Token Transmission:
- ✅ Always use HTTPS in production
- ✅ Send tokens in
Authorization: Bearer <token>header - ❌ Never send tokens in URL query parameters
- ❌ Never log JWT tokens in production
Token Duration:
// Balance security vs. usability
{
"jwt_duration": 3600 // 1 hour recommended for most applications
}Token Validation:
// SDK automatically validates:
// 1. JWT signature (cryptographic verification)
// 2. JWT expiration (time-based validation)
// 3. Session existence (state validation)
// 4. Session status (not closed/invalidated)3. Session Management Security
Session Storage:
// ✅ Production - Use persistent storage with encryption
const sessionStore = new SQLiteStore({
db: 'sessions.db',
dir: './data',
table: 'sessions'
});
// Ensure database file has restrictive permissions
// chmod 600 ./data/sessions.dbSession Invalidation:
// Always invalidate sessions on logout
app.post('/api/logout', authenticate, async (req, res) => {
await roditClient.logout_client(req, res);
// Session is now closed and token is invalidated
});
// Automatic cleanup of expired sessions
const sessionManager = roditClient.getSessionManager();
await sessionManager.runManualCleanup();4. Rate Limiting
Implement rate limiting to prevent abuse:
const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;
const maxRequests = parseInt(metadata.max_requests);
const windowSeconds = parseInt(metadata.maxrq_window);
const rateLimiter = roditClient.getRateLimitMiddleware();
app.use(rateLimiter(maxRequests, windowSeconds));Additional Protection:
// Add IP-based rate limiting for login endpoint
const loginRateLimiter = ratelimitmw(5, 900); // 5 attempts per 15 minutes
app.post('/api/login', loginRateLimiter, (req, res) => {
return roditClient.login_client(req, res);
});5. Input Validation
Always validate input data:
// ✅ Validate before processing
app.post('/api/cruda/create', authenticate, authorize, async (req, res) => {
const { comment, author } = req.body;
// Validate input
if (!comment || typeof comment !== 'string') {
return res.status(400).json({ error: 'Invalid comment' });
}
if (comment.length > 1000) {
return res.status(400).json({ error: 'Comment too long' });
}
// Sanitize input
const sanitizedComment = comment.trim();
// Process request
// ...
});6. Error Handling
Never expose sensitive information in errors:
// ✅ Production - Generic error messages
const isProduction = process.env.NODE_ENV === 'production';
try {
// Operation
} catch (error) {
logger.errorWithContext('Operation failed', {
component: 'API',
error: error.message,
stack: error.stack,
requestId: req.requestId
}, error);
res.status(500).json({
error: isProduction ? 'Internal server error' : error.message,
requestId: req.requestId
});
}7. HTTPS/TLS Configuration
Always use HTTPS in production:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('/path/to/private-key.pem'),
cert: fs.readFileSync('/path/to/certificate.pem'),
// Use strong TLS settings
minVersion: 'TLSv1.2',
ciphers: 'HIGH:!aNULL:!MD5'
};
const server = https.createServer(options, app);
server.listen(443);8. CORS Configuration
Configure CORS restrictively:
const cors = require('cors');
// ✅ Production - Whitelist specific origins
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// ❌ Never use in production
app.use(cors({ origin: '*' }));9. Logging Security
Avoid logging sensitive data:
// ✅ Safe logging
logger.info('User authenticated', {
component: 'Authentication',
userId: user.id,
requestId: req.requestId
});
// ❌ Never log sensitive data
logger.info('Authentication attempt', {
password: req.body.password, // DON'T DO THIS
privateKey: credentials.private_key, // DON'T DO THIS
jwtToken: token // DON'T DO THIS
});10. Webhook Security
Validate webhook endpoints:
// Webhooks are sent to URLs configured in RODiT token
// Ensure webhook_url uses HTTPS
{
"webhook_url": "https://webhook.example.com:3444", // ✅ HTTPS
"webhook_cidr": "10.0.0.0/8" // Restrict to specific IP ranges
}
// Implement webhook signature verification
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}11. Permission Validation
Always validate permissions before operations:
// ✅ Check permissions explicitly
app.post('/api/admin/users', authenticate, authorize, async (req, res) => {
const client = req.app.locals.roditClient;
// Double-check permission
if (!client.isOperationPermitted('POST', '/api/admin/users')) {
return res.status(403).json({ error: 'Forbidden' });
}
// Proceed with operation
});12. Security Headers
Add security headers:
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// Additional security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});13. Audit Logging
Log security-relevant events:
const logger = roditClient.getLogger();
// Log authentication events
logger.info('Login attempt', {
component: 'Security',
event: 'login_attempt',
userId: req.body.roditid,
ip: req.ip,
userAgent: req.headers['user-agent'],
requestId: req.requestId
});
// Log authorization failures
logger.warn('Authorization failed', {
component: 'Security',
event: 'authorization_failure',
userId: req.user.id,
path: req.path,
method: req.method,
ip: req.ip,
requestId: req.requestId
});
// Log suspicious activity
if (failedAttempts > 5) {
logger.error('Multiple failed login attempts', {
component: 'Security',
event: 'brute_force_attempt',
ip: req.ip,
attempts: failedAttempts,
requestId: req.requestId
});
}14. Environment Separation
Maintain strict environment separation:
# Development
export NODE_ENV=development
export LOG_LEVEL=debug
export RODIT_NEAR_CREDENTIALS_SOURCE=file
# Staging
export NODE_ENV=production
export LOG_LEVEL=info
export RODIT_NEAR_CREDENTIALS_SOURCE=vault
# Production
export NODE_ENV=production
export LOG_LEVEL=warn
export RODIT_NEAR_CREDENTIALS_SOURCE=vaultSecurity Checklist
Before deploying to production:
- All credentials stored in Vault
- HTTPS/TLS enabled with strong ciphers
- JWT duration set appropriately (≤ 2 hours)
- Rate limiting configured
- CORS configured with whitelist
- Security headers enabled (helmet)
- Input validation on all endpoints
- Error messages don't expose sensitive data
- Logging doesn't include sensitive data
- Session storage uses persistent backend
- Automatic session cleanup enabled
- Audit logging for security events
- File permissions restrictive (600 for sensitive files)
- Environment variables properly configured
- NODE_ENV set to 'production'
Troubleshooting
Common Issues
1. Authentication Middleware Errors
Problem: roditClient.authenticate is not a function or Cannot read properties of undefined
Solution: Ensure client is initialized and stored in app.locals:
// ✅ Correct - Check client availability
const authenticate = (req, res, next) => {
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({ error: 'Authentication service unavailable' });
}
return client.authenticate(req, res, next);
};
// ❌ Wrong - Direct access without checking
const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
// This fails if roditClient is not initialized yet2. Configuration Not Found
Problem: Failed to initialize RODiT configuration
Solutions:
# Check environment variables
echo $RODIT_NEAR_CREDENTIALS_SOURCE # Should be 'vault' or 'file'
echo $VAULT_ENDPOINT
echo $NEAR_CONTRACT_ID
echo $SERVICE_NAME
# For vault-based credentials
export RODIT_NEAR_CREDENTIALS_SOURCE=vault
export VAULT_ENDPOINT=https://vault.example.com
export VAULT_ROLE_ID=your-role-id
export VAULT_SECRET_ID=your-secret-id
# For file-based credentials (development)
export RODIT_NEAR_CREDENTIALS_SOURCE=file
export CREDENTIALS_FILE_PATH=./credentials/rodit-credentials.jsonVerify configuration:
const config = roditClient.getConfig();
console.log('NEAR_CONTRACT_ID:', config.get('NEAR_CONTRACT_ID'));
console.log('SERVICE_NAME:', config.get('SERVICE_NAME'));3. Missing App.locals Client
Problem: RoditClient not available in app.locals or Cannot read properties of undefined (reading 'roditClient')
Solution: Ensure client is stored during initialization:
async function startServer() {
try {
// Initialize client
const roditClient = await RoditClient.create('server');
// Store in app.locals BEFORE mounting routes
app.locals.roditClient = roditClient;
// Verify it's stored
if (!app.locals.roditClient) {
throw new Error('Failed to store roditClient in app.locals');
}
// Now mount routes
const authenticate = (req, res, next) => roditClient.authenticate(req, res, next);
app.use('/api/protected', authenticate, protectedRoutes);
app.listen(port);
} catch (error) {
console.error('Server initialization failed:', error);
process.exit(1);
}
}4. Permission Denied Errors
Problem: Routes return 403 Forbidden
Debug steps:
// Check token permissions
const configObject = await roditClient.getConfigOwnRodit();
const permissionedRoutes = JSON.parse(
configObject.own_rodit.metadata.permissioned_routes || '{}'
);
console.log('Configured permissions:', permissionedRoutes);
// Check specific operation
const hasPermission = roditClient.isOperationPermitted('POST', '/api/cruda/create');
console.log('Has permission:', hasPermission);
// Verify route path matches exactly
console.log('Requested path:', req.path); // Must match permission key exactlyCommon issues:
- Route path doesn't match permission key exactly (e.g.,
/api/cruda/createvs/cruda/create) - HTTP method not allowed in permission configuration
- Permission format incorrect (should be
"+0"for all methods) - Client token has different permissions than server token
5. Session Not Found Errors
Problem: 401 Unauthorized - session_not_found
Cause: JWT token contains session ID that doesn't exist in session storage
Solutions:
// Verify session storage is configured
const sessionManager = roditClient.getSessionManager();
const storageInfo = await sessionManager.getStorageInfo();
console.log('Storage type:', storageInfo.storageType);
console.log('Active sessions:', storageInfo.sessionCount);
// Check if token is invalidated
const isInvalidated = await sessionManager.isTokenInvalidated(jwtToken);
console.log('Token invalidated:', isInvalidated);
// Enumerate sessions via storage for debugging
const allSessions = await sessionManager.storage.getAll();
console.log('Active sessions:', allSessions.filter(s => s.status === 'active').length);Common causes:
- Server restarted with in-memory storage (sessions lost)
- Session expired
- Token was invalidated by logout
- Session storage not configured properly
Solution: Use persistent storage (SQLite or Redis) for production
6. Logging Issues
Problem: Logs not appearing in Loki or console
Solutions:
# Check logging configuration
export LOG_LEVEL=debug # Enable debug logging
export LOKI_URL=https://loki.example.com:3100
export LOKI_BASIC_AUTH=username:password// Test logger directly
const logger = roditClient.getLogger();
logger.info('Test message', { component: 'Test' });
logger.error('Test error', { component: 'Test' });
// Check if Loki transport is configured
const transports = logger.transports;
console.log('Logger transports:', transports.map(t => t.name));Debug Mode
Enable debug logging for troubleshooting:
export LOG_LEVEL=debug # Use 'debug' or 'trace' for development modeThis will provide detailed information about:
- Authentication flows and token validation
- Configuration loading from Vault/files
- Permission checks and route matching
- Session creation and validation
- Network requests to portal/blockchain
- Internal SDK operations
- Request/response details
Example debug output:
const logger = roditClient.getLogger();
// Enable debug logging programmatically
logger.level = 'debug';
// Debug authentication
logger.debug('Authenticating request', {
component: 'Authentication',
hasAuthHeader: !!req.headers.authorization,
path: req.path,
method: req.method
});Health Checks
Implement comprehensive health check endpoints:
app.get('/health', async (req, res) => {
try {
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({
status: 'error',
message: 'RoditClient not available'
});
}
const configObject = await client.getConfigOwnRodit();
const sessionManager = client.getSessionManager();
const performanceService = client.getPerformanceService();
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
logLevel: config.get('LOG_LEVEL', 'info'),
components: {
roditClient: !!client,
configuration: !!(configObject && configObject.own_rodit),
sessionManager: !!sessionManager,
performanceService: !!performanceService
},
metrics: {
activeSessions: await sessionManager.getActiveSessionCount(),
totalRequests: performanceService.getRequestCount(),
errorCount: performanceService.getErrorCount()
},
roditToken: {
tokenId: configObject?.own_rodit?.token_id,
apiUrl: configObject?.own_rodit?.metadata?.subjectuniqueidentifier_url,
jwtDuration: configObject?.own_rodit?.metadata?.jwt_duration
}
};
res.json(health);
} catch (error) {
res.status(503).json({
status: 'error',
message: error.message,
timestamp: new Date().toISOString()
});
}
});
// Readiness check (for Kubernetes)
app.get('/ready', async (req, res) => {
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({ ready: false });
}
try {
const configObject = await client.getConfigOwnRodit();
const ready = !!(configObject && configObject.own_rodit);
res.status(ready ? 200 : 503).json({ ready });
} catch (error) {
res.status(503).json({ ready: false, error: error.message });
}
});
// Liveness check (for Kubernetes)
app.get('/live', (req, res) => {
res.json({ alive: true });
});Support
For additional support:
- Check the debug logs with
LOG_LEVEL=debug - Verify your RODiT token configuration
- Test with the health check endpoint
- Review the authentication flow in the logs
- Ensure all required environment variables are set
Version History
Version 4.0.2 (Current)
Features:
- ✅ Session storage configuration exports (
setExpressSessionStore,configureStorageFromConfig, etc.) - ✅ Test instance support with
RoditClient.createTestInstance()for concurrent testing - ✅ Comprehensive session management with pluggable storage backends
- ✅ Direct Loki logging integration via winston-loki
- ✅ Performance tracking and metrics collection
- ✅ Webhook support with graceful error handling
- ✅ Rate limiting middleware with RODiT token configuration
- ✅ Authorization middleware with route-based permissions
- ✅ Graceful shutdown support
Improvements:
- Enhanced error messages with request context
- Better logging with structured context
- Improved session cleanup and token invalidation
- Support for SQLite, Redis, and in-memory session storage
- Environment-specific configuration (NODE_ENV vs LOG_LEVEL separation)
Bug Fixes:
- Fixed session storage initialization order
- Corrected API endpoint resolution from RODiT metadata
- Fixed JWT token validation and session state checking
- Resolved circular dependency issues in imports
Version 2.8.x
Features:
- Initial session management implementation
- Basic authentication and authorization
- RODiT token validation
- Portal authentication support
Migration Guide
Migrating to 4.0.2
Session Storage Configuration:
// Old (not available):
// Had to import from deep path
// New (available from main SDK):
const { RoditClient, setExpressSessionStore } = require('@rodit/rodit-auth-be');
const sessionStore = new SQLiteStore({ db: 'sessions.db', dir: './data' });
setExpressSessionStore(sessionStore);Test Instance Creation:
// Old:
// Had to manually create with testMode option
// New (simplified):
const testClient = await RoditClient.createTestInstance();License
Copyright (c) 2025 Discernible Inc. All rights reserved.