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 logging in Express.js applications.
Table of Contents
- Quick Start
- Core Concepts
- Mutual Authentication
- Authorization & Permissions
- Self-Configuration
- Logging & Monitoring
- Advanced Usage
- API Reference
- Best Practices
- Troubleshooting
Quick Start
Installation
npm install @rodit/rodit-auth-beBasic Server Setup
const express = require('express');
const { RoditClient, logger, loggingmw, config } = require('@rodit/rodit-auth-be');
const { ulid } = require('ulid');
const app = express();
let roditClient;
// Configure Express middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(loggingmw);
// Request context middleware
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || ulid();
req.startTime = Date.now();
next();
});
// Authentication routes
app.post('/login', (req, res, next) => {
req.logAction = "login-attempt";
next();
}, roditClient.authenticate, (req, res) => {
res.json({
success: true,
message: "Authentication successful",
user: { id: req.user?.id },
requestId: req.requestId
});
});
app.post('/logout', roditClient.authenticate, (req, res) => {
roditClient.logoutClient(req, res);
});
// Protected routes
app.use('/api/protected', roditClient.authenticate, protectedRoutes);
// Server startup with SDK initialization
async function startServer() {
try {
// Initialize RODiT client
roditClient = await RoditClient.create('portal');
// Store client in app.locals for route access
app.locals.roditClient = roditClient;
const port = config.get('SERVERPORT');
app.listen(port, () => {
logger.info(`RODiT Authentication Server running on port ${port}`);
});
} catch (error) {
logger.error('Server initialization failed', {
error: error.message,
stack: error.stack
});
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() - Shared Instance: Store in
app.localsfor access across routes - Self-Configuring: Automatically loads configuration from vault or files
- Encapsulated: All SDK functionality accessed through the client instance
App.locals Pattern
Following the successful clienttestapi-rodit implementation, store the initialized client in app.locals:
// In main app.js
app.locals.roditClient = await RoditClient.create('portal');
// In route modules
const authenticate = (req, res, next) => {
const client = req.app.locals.roditClient;
return client.authenticateApiCall(req, res, next);
};Mutual Authentication
RODiT-Based Authentication
RODiT provides cryptographic mutual authentication using blockchain-verified identities:
// Authentication request format
{
"roditid": "your-rodit-id",
"timestamp": 1640995200,
"roditid_base64url_signature": "base64url-encoded-signature"
}Implementation in Routes
// Protected route with authentication
app.get('/api/data', roditClient.authenticate, (req, res) => {
// req.user contains authenticated user information
res.json({
message: 'Authenticated data',
user: req.user,
requestId: req.requestId
});
});Authentication Flow
- Client sends RODiT and cryptographic signature
- SDK verifies signature against blockchain records
- JWT token issued for session management
- Subsequent requests use JWT for performance
- Token refresh handled automatically
Authorization & Permissions
Route-Based Permissions
Configure permissions in your RODiT token metadata:
{
"permissioned_routes": {
"/api/admin": ["GET", "POST", "PUT", "DELETE"],
"/api/users": ["GET", "POST"],
"/api/public": ["GET"]
}
}Permission Validation Middleware
// Apply both authentication and permission validation
app.use('/api/admin',
roditClient.authenticate,
roditClient.authorize,
adminRoutes
);
// In route handlers
app.get('/api/sensitive-data',
roditClient.authenticate,
roditClient.authorize,
(req, res) => {
// Only accessible if user has permission for GET /api/sensitive-data
res.json({ data: 'sensitive information' });
}
);Dynamic Permission Checking
// Check permissions programmatically
const hasPermission = roditClient.isOperationPermitted('POST', '/api/admin/users');
if (hasPermission) {
// Proceed with operation
}Self-Configuration
Automatic Configuration Loading
The SDK automatically configures itself from multiple sources:
- Vault Credentials (Production)
- File-based Credentials (Development)
- Environment Variables
- Configuration Files
- SDK Defaults (Fallback)
Vault-Based Configuration
# 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.testnetAccessing Configuration
// Get complete RODiT configuration
const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;
// Access specific configuration values
const jwtDuration = metadata.jwt_duration;
const allowedRoutes = JSON.parse(metadata.permissioned_routes || '{}');
const apiEndpoint = metadata.subjectuniqueidentifier_url;
// Use SDK config wrapper for application settings
const { config } = require('@rodit/rodit-auth-be');
const serverPort = config.get('SERVERPORT');
const logLevel = config.get('LOG_LEVEL', 'info');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 Integration
Configure centralized logging with Grafana Loki:
# Environment variables
export LOKI_URL=https://loki.example.com:3100
export LOKI_BASIC_AUTH=username:password
export LOKI_TLS_SKIP_VERIFY=true # Only for testing
export LOG_LEVEL=info// Custom logger injection for advanced scenarios
const winston = require('winston');
const LokiTransport = require('winston-loki');
const customLogger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [
new winston.transports.Console(),
new LokiTransport({
host: process.env.LOKI_URL,
basicAuth: process.env.LOKI_BASIC_AUTH,
labels: { app: 'my-service', component: 'rodit-sdk' },
json: true,
batching: true
})
]
});
logger.setLogger(customLogger);Performance Monitoring
// Request performance tracking
app.use((req, res, next) => {
req.startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - req.startTime;
logger.metric('request_duration_ms', duration, {
method: req.method,
path: req.path,
status: res.statusCode
});
logger.debugWithContext('Request completed', {
component: 'API',
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration,
requestId: req.requestId
});
});
next();
});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.authenticateApiCall(req, res, next);
};
const validatePermissions = (req, res, next) => {
const client = req.app.locals.roditClient;
if (!client) {
return res.status(503).json({ error: 'Authentication service unavailable' });
}
return client.validatePermissions(req, res, next);
};
// Protected route with full authentication and authorization
router.get('/data', authenticate, validatePermissions, 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;Custom Signer Classes
For specialized signing operations, create classes that use the shared RoditClient:
// services/DocumentSigner.js
class DocumentSigner {
constructor(roditClient) {
this.roditClient = roditClient;
this.initialized = false;
}
async initialize() {
if (this.initialized) return;
// Get configuration from the shared client
const configObject = await this.roditClient.getConfigOwnRodit();
this.metadata = configObject.own_rodit.metadata;
// Get credentials through the client's manager
const credentials = await this.roditClient.getRoditManager().getCredentials('portal');
this.signingKey = credentials.signing_bytes_key;
this.initialized = true;
logger.info('DocumentSigner initialized successfully');
}
async signDocument(document) {
if (!this.initialized) {
await this.initialize();
}
// Signing logic using this.signingKey
const signature = this.createSignature(document);
return signature;
}
}
// Usage in routes
router.post('/sign-document', authenticate, async (req, res) => {
try {
// Get or create signer instance from app.locals
if (!req.app.locals.documentSigner) {
const roditClient = req.app.locals.roditClient;
req.app.locals.documentSigner = new DocumentSigner(roditClient);
}
const signer = req.app.locals.documentSigner;
const signature = await signer.signDocument(req.body.document);
res.json({ signature, requestId: req.requestId });
} catch (error) {
logger.errorWithContext('Document signing failed', {
component: 'DocumentSigner',
requestId: req.requestId,
error: error.message
}, error);
res.status(500).json({
error: 'Document signing failed',
requestId: req.requestId
});
}
});Session Management
// Access session manager through the client
const sessionManager = roditClient.getSessionManager();
// Create custom session data
const session = sessionManager.createSession(userId, {
loginTime: Date.now(),
ipAddress: req.ip,
userAgent: req.get('User-Agent')
});
// Validate sessions
const isValid = sessionManager.validateSession(sessionId, token);
// Clean up expired sessions
const cleanupResult = sessionManager.removeExpiredSessions();
logger.info(`Cleaned up ${cleanupResult} expired sessions`);Webhook Integration
// Send webhooks through the client
const webhookData = {
event: 'user_login',
userId: req.user.id,
timestamp: Date.now(),
metadata: {
ip: req.ip,
userAgent: req.get('User-Agent')
}
};
try {
const result = await roditClient.sendWebhook(webhookData);
logger.info('Webhook sent successfully', { result });
} catch (error) {
logger.error('Webhook failed', { error: error.message });
}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('portal'); // or 'client'Parameters:
role(string): Client role - 'portal' for server applications, 'client' for client applications
Returns: Promise
Instance Methods
authenticateApiCall(req, res, next)
Express middleware for authenticating API requests.
app.use('/api/protected', roditClient.authenticate, handler);validatePermissions(req, res, next)
Express middleware for validating route permissions.
app.use('/api/admin',
roditClient.authenticate,
roditClient.authorize,
handler
);logoutClient(req, res)
Handle client logout requests.
app.post('/logout', roditClient.authenticate, (req, res) => {
roditClient.logoutClient(req, res);
});getConfigOwnRodit()
Get the complete RODiT configuration including token metadata.
const configObject = await roditClient.getConfigOwnRodit();
const metadata = configObject.own_rodit.metadata;Returns: Promise
isOperationPermitted(method, path)
Check if an operation is permitted based on token permissions.
const hasPermission = roditClient.isOperationPermitted('POST', '/api/admin/users');Parameters:
method(string): HTTP method (GET, POST, PUT, DELETE, etc.)path(string): API path
Returns: boolean - True if operation is permitted
getStateManager()
Get the state manager instance.
const stateManager = roditClient.getStateManager();getRoditManager()
Get the RODiT manager instance.
const roditManager = roditClient.getRoditManager();getSessionManager()
Get the session manager instance.
const sessionManager = roditClient.getSessionManager();getLogger()
Get the logger instance.
const logger = roditClient.getLogger();getRateLimitMiddleware()
Get the rate limiting middleware factory.
const rateLimiter = roditClient.getRateLimitMiddleware();
const middleware = rateLimiter(100, 900); // 100 requests per 15 minutessendWebhook(data)
Send a webhook with the provided data.
const result = await roditClient.sendWebhook({
event: 'user_action',
data: { userId: '123', action: 'login' }
});Configuration System
Environment Variables
Required 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
export VAULT_RODIT_KEYVALUE_PATH=secret/rodit
export SERVICE_NAME=your-service-name
export NEAR_CONTRACT_ID=your-contract.testnetLogging configuration:
export LOG_LEVEL=info
export LOKI_URL=https://loki.example.com:3100
export LOKI_BASIC_AUTH=username:password
export LOKI_TLS_SKIP_VERIFY=true # Only for testingApplication configuration:
export SERVERPORT=3000
export API_DEFAULT_OPTIONS_LOG_DIR=/app/logs
export API_DEFAULT_OPTIONS_DB_PATH=/app/data/database.dbConfiguration Files
Place configuration in config/default.json, config/production.json, etc.:
{
"NEAR_CONTRACT_ID": "your-contract.testnet",
"SERVICE_NAME": "your-service",
"SERVERPORT": 3000,
"API_DEFAULT_OPTIONS": {
"LOG_DIR": "/app/logs",
"DB_PATH": "/app/data/database.db"
},
"METHOD_PERMISSION_MAP": {
"list_agents": ["entityAndProperties", "entityOnly"],
"create_user": ["admin", "manager"]
},
"SECURITY_OPTIONS": {
"SILENT_LOGIN_FAILURES": false,
"JWT_DURATION": 3600
}
}RODiT Token Metadata Fields
When you call roditClient.getConfigOwnRodit(), you get access to these metadata fields:
token_id: Unique RODIT token identifierallowed_cidr: Permitted IP address ranges (CIDR format)allowed_iso3166list: Geographic restrictions (JSON string)jwt_duration: JWT token lifetime in secondsmax_requests: Rate limit - maximum requests per windowmaxrq_window: Rate limit - time window in secondsnot_before/not_after: Token validity periodopenapijson_url: OpenAPI specification URLpermissioned_routes: Allowed API routes and methods (JSON string)serviceprovider_id: Blockchain contract and service provider infoserviceprovider_signature: Cryptographic signature for verificationsubjectuniqueidentifier_url: Primary API service endpointwebhook_cidr: Allowed IP ranges for webhookswebhook_url: 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('portal');
app.locals.roditClient = roditClient;
// ... rest of server setup
}
// ❌ Bad - Multiple initializations
app.get('/route1', async (req, res) => {
const client = await RoditClient.create('portal'); // 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
const authenticate = (req, res, next) => {
const client = req.app.locals.roditClient;
return client.authenticateApiCall(req, res, next);
};
// ❌ Bad - Direct SDK imports in routes
const { stateManager } = require('@rodit/rodit-auth-be');
const config = await stateManager.getConfigOwnRodit(); // Don't do this3. Proper Error Handling
Always wrap SDK operations in try-catch blocks:
// ✅ Good - Comprehensive error handling
app.get('/api/data', authenticate, async (req, res) => {
const startTime = Date.now();
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
logger.infoWithContext('User action completed', {
component: 'UserService',
action: 'updateProfile',
userId: user.id,
requestId: req.requestId,
duration: Date.now() - startTime,
changes: Object.keys(updates)
});
// ❌ 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 isProduction = process.env.NODE_ENV === 'production';
const logLevel = process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug');
// Production should use vault credentials
if (isProduction && process.env.RODIT_NEAR_CREDENTIALS_SOURCE !== 'vault') {
logger.warn('Production environment should use vault credentials');
}6. Graceful Shutdown
Implement proper shutdown handling:
// ✅ Good - Graceful shutdown
const shutdown = async (signal) => {
logger.info('Shutting down gracefully', {
component: 'AppLifecycle',
signal: signal || 'unknown',
time: new Date().toISOString()
});
if (server) {
server.close(() => {
logger.info('HTTP server closed');
process.exit(0);
});
}
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));7. Request Context
Always include request context in operations:
// ✅ Good - Request context tracking
app.use((req, res, next) => {
req.requestId = req.headers['x-request-id'] || ulid();
req.startTime = Date.now();
next();
});
// Include context in all operations
logger.infoWithContext('Processing request', {
component: 'API',
method: req.method,
path: req.path,
requestId: req.requestId,
userId: req.user?.id
});Troubleshooting
Common Issues
1. Authentication Middleware Errors
Problem: roditClient.authenticate_apicall is not a function
Solution: Use the correct method name and binding:
// ❌ Wrong (old pattern)
roditClient.authenticate_apicall
// ❌ Verbose (works but unnecessary)
roditClient.authenticateApiCall.bind(roditClient)
// ✅ Correct (clean and simple)
roditClient.authenticate2. Configuration Not Found
Problem: Failed to initialize RODiT configuration
Solutions:
- Ensure vault credentials are properly set
- Check
RODIT_NEAR_CREDENTIALS_SOURCEenvironment variable - Verify vault connectivity and permissions
- For development, ensure credentials file exists
3. Missing App.locals Client
Problem: RoditClient not available in app.locals
Solution: Ensure client is stored during initialization:
async function startServer() {
const roditClient = await RoditClient.create('portal');
app.locals.roditClient = roditClient; // Don't forget this line
}4. Permission Denied Errors
Problem: Routes return 403 Forbidden
Solutions:
- Check
permissioned_routesin your RODiT token metadata - Verify the route path matches the permission pattern
- Ensure HTTP method is allowed
- Use
roditClient.isOperationPermitted()to debug
5. Logging Issues
Problem: Logs not appearing in Loki
Solutions:
- Verify
LOKI_URLand credentials - Check network connectivity to Loki instance
- Ensure proper TLS configuration
- Test with console logging first
Debug Mode
Enable debug logging for troubleshooting:
export LOG_LEVEL=debugThis will provide detailed information about:
- Authentication flows
- Configuration loading
- Permission checks
- Network requests
- Internal SDK operations
Health Checks
Implement health check endpoints to verify SDK status:
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 hasValidConfig = !!(configObject && configObject.own_rodit);
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
roditClient: !!client,
configuration: hasValidConfig,
environment: process.env.NODE_ENV || 'development'
});
} catch (error) {
res.status(503).json({
status: 'error',
message: error.message,
timestamp: new Date().toISOString()
});
}
});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
License
Copyright (c) 2025 Discernible Inc. All rights reserved.