UNPKG

@compyai/securetoken

Version:

A modular, encrypted, DB-agnostic token system with optional refresh support, honeypot protection, and revocation. A secure alternative to JWT.

601 lines (485 loc) 15.6 kB
# Secure Token Toolkit A robust Node.js library for creating and managing secure JWT-like tokens with built-in security features including honeypot protection, session management, and advanced binding options. ## Features - **AES-256-GCM Encryption**: All token payloads are encrypted with industry-standard encryption - **HMAC-SHA512 Signing**: Cryptographic signatures prevent token tampering - **Honeypot Protection**: Built-in protection against brute force attacks with decoy tokens - **Session Management**: Optional session storage and revocation capabilities - **IP & Fingerprint Binding**: Bind tokens to specific IP addresses or browser fingerprints - **Refresh Token Support**: Secure token refresh mechanism - **Timing Attack Protection**: Uses timing-safe comparison for signature verification ## Installation ```bash npm install @compyai/securetoken ``` ## Quick Start ```javascript const { createSecureTokenToolkit } = require("@compyai/securetoken"); // Initialize the toolkit with encryption keys const toolkit = createSecureTokenToolkit({ encKey: "your-256-bit-encryption-key-in-hex", // 64 hex characters hmacKey: "your-256-bit-hmac-key-in-hex", // 64 hex characters }); // Create a token const { accessToken, refreshToken } = await toolkit.createToken( { userId: 123, role: "user" }, { expiresIn: 900, refresh: true } ); // Verify a token try { const payload = await toolkit.verifyToken(accessToken); console.log("User ID:", payload.userId); } catch (error) { console.error("Token verification failed:", error.message); } ``` ## How It Works ```mermaid graph TB A[Client Request] --> B{Token Valid?} B -->|No| C[Check Honeypot] C -->|Under Threshold| D[Return Error] C -->|Over Threshold| E[Return Decoy Token] E --> F[Log Attack Attempt] B -->|Yes| G[Decrypt Payload] G --> H{Check Bindings} H -->|IP/Fingerprint Mismatch| I[Return Error] H -->|Valid| J{Session Active?} J -->|Revoked| K[Return Error] J -->|Active| L[Return Payload] style A fill:#e1f5fe style L fill:#e8f5e8 style D fill:#ffebee style E fill:#fff3e0 style F fill:#fff3e0 ``` ## Configuration ### Basic Configuration ```javascript const config = { encKey: "your-256-bit-encryption-key-in-hex", hmacKey: "your-256-bit-hmac-key-in-hex", }; ``` ### Advanced Configuration with Honeypot ```javascript const config = { encKey: "your-256-bit-encryption-key-in-hex", hmacKey: "your-256-bit-hmac-key-in-hex", honeypot: { enabled: true, failThreshold: 5, decoyPayload: { userId: 999, role: "admin" }, slowResponses: true, onTriggered: (failInfo) => { console.log("Honeypot triggered:", failInfo); }, onDecoyUse: (payload) => { console.log("Decoy token used:", payload); }, }, store: { saveSession: async (sessionId, data) => { // Save session to your database }, saveRefresh: async (sessionId, data) => { // Save refresh token info to your database }, isSessionRevoked: async (sessionId) => { // Check if session is revoked return false; }, isRefreshRevoked: async (sessionId) => { // Check if refresh token is revoked return false; }, revokeRefresh: async (sessionId) => { // Revoke refresh token }, }, }; ``` ## Session Store Backends The toolkit supports various session storage backends. Here are complete implementation examples: ### Redis Store (Recommended for Production) ```javascript const Redis = require("ioredis"); const { createSecureTokenToolkit } = require("@compyai/securetoken"); const redis = new Redis({ host: "localhost", port: 6379, retryDelayOnFailover: 100, maxRetriesPerRequest: 3, }); const redisStore = { async saveSession(sessionId, data) { const key = `session:${sessionId}`; await redis.setex( key, data.exp - Math.floor(Date.now() / 1000), JSON.stringify(data) ); }, async saveRefresh(sessionId, data) { const key = `refresh:${sessionId}`; await redis.setex( key, data.exp - Math.floor(Date.now() / 1000), JSON.stringify(data) ); }, async isSessionRevoked(sessionId) { const key = `session:${sessionId}`; const result = await redis.get(key); return result === null; }, async isRefreshRevoked(sessionId) { const key = `refresh:${sessionId}`; const result = await redis.get(key); return result === null; }, async revokeRefresh(sessionId) { const key = `refresh:${sessionId}`; await redis.del(key); }, async revokeSession(sessionId) { const sessionKey = `session:${sessionId}`; const refreshKey = `refresh:${sessionId}`; await redis.del([sessionKey, refreshKey]); }, }; const toolkit = createSecureTokenToolkit({ encKey: process.env.TOKEN_ENC_KEY, hmacKey: process.env.TOKEN_HMAC_KEY, store: redisStore, }); ``` ### Database Store (PostgreSQL Example) ```javascript const { Pool } = require("pg"); const { createSecureTokenToolkit } = require("@compyai/securetoken"); const pool = new Pool({ user: "username", host: "localhost", database: "myapp", password: "password", port: 5432, }); // Database schema: // CREATE TABLE sessions ( // session_id UUID PRIMARY KEY, // data JSONB NOT NULL, // expires_at TIMESTAMP NOT NULL, // created_at TIMESTAMP DEFAULT NOW() // ); // // CREATE TABLE refresh_tokens ( // session_id UUID PRIMARY KEY, // data JSONB NOT NULL, // expires_at TIMESTAMP NOT NULL, // created_at TIMESTAMP DEFAULT NOW() // ); const dbStore = { async saveSession(sessionId, data) { const expiresAt = new Date(data.exp * 1000); await pool.query( "INSERT INTO sessions (session_id, data, expires_at) VALUES ($1, $2, $3) ON CONFLICT (session_id) DO UPDATE SET data = $2, expires_at = $3", [sessionId, JSON.stringify(data), expiresAt] ); }, async saveRefresh(sessionId, data) { const expiresAt = new Date(data.exp * 1000); await pool.query( "INSERT INTO refresh_tokens (session_id, data, expires_at) VALUES ($1, $2, $3) ON CONFLICT (session_id) DO UPDATE SET data = $2, expires_at = $3", [sessionId, JSON.stringify(data), expiresAt] ); }, async isSessionRevoked(sessionId) { const result = await pool.query( "SELECT 1 FROM sessions WHERE session_id = $1 AND expires_at > NOW()", [sessionId] ); return result.rows.length === 0; }, async isRefreshRevoked(sessionId) { const result = await pool.query( "SELECT 1 FROM refresh_tokens WHERE session_id = $1 AND expires_at > NOW()", [sessionId] ); return result.rows.length === 0; }, async revokeRefresh(sessionId) { await pool.query("DELETE FROM refresh_tokens WHERE session_id = $1", [ sessionId, ]); }, async revokeSession(sessionId) { const client = await pool.connect(); try { await client.query("BEGIN"); await client.query("DELETE FROM sessions WHERE session_id = $1", [ sessionId, ]); await client.query("DELETE FROM refresh_tokens WHERE session_id = $1", [ sessionId, ]); await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } }, }; const toolkit = createSecureTokenToolkit({ encKey: process.env.TOKEN_ENC_KEY, hmacKey: process.env.TOKEN_HMAC_KEY, store: dbStore, }); ``` ### In-Memory Store (Development/Testing) ```javascript const { createSecureTokenToolkit } = require("@compyai/securetoken"); class MemoryStore { constructor() { this.sessions = new Map(); this.refreshTokens = new Map(); this.cleanup(); } async saveSession(sessionId, data) { this.sessions.set(sessionId, { data, expiresAt: data.exp * 1000, }); } async saveRefresh(sessionId, data) { this.refreshTokens.set(sessionId, { data, expiresAt: data.exp * 1000, }); } async isSessionRevoked(sessionId) { const session = this.sessions.get(sessionId); if (!session) return true; if (Date.now() > session.expiresAt) { this.sessions.delete(sessionId); return true; } return false; } async isRefreshRevoked(sessionId) { const refresh = this.refreshTokens.get(sessionId); if (!refresh) return true; if (Date.now() > refresh.expiresAt) { this.refreshTokens.delete(sessionId); return true; } return false; } async revokeRefresh(sessionId) { this.refreshTokens.delete(sessionId); } async revokeSession(sessionId) { this.sessions.delete(sessionId); this.refreshTokens.delete(sessionId); } // Cleanup expired entries every 5 minutes cleanup() { setInterval(() => { const now = Date.now(); for (const [sessionId, session] of this.sessions.entries()) { if (now > session.expiresAt) { this.sessions.delete(sessionId); } } for (const [sessionId, refresh] of this.refreshTokens.entries()) { if (now > refresh.expiresAt) { this.refreshTokens.delete(sessionId); } } }, 5 * 60 * 1000); } } const memoryStore = new MemoryStore(); const toolkit = createSecureTokenToolkit({ encKey: process.env.TOKEN_ENC_KEY, hmacKey: process.env.TOKEN_HMAC_KEY, store: memoryStore, }); ``` ### MongoDB Store ```javascript const { MongoClient } = require("mongodb"); const { createSecureTokenToolkit } = require("@compyai/securetoken"); const client = new MongoClient("mongodb://localhost:27017"); const db = client.db("myapp"); // Create indexes for automatic expiration // db.sessions.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 }); // db.refreshTokens.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 }); const mongoStore = { async saveSession(sessionId, data) { await db.collection("sessions").replaceOne( { sessionId }, { sessionId, data, expiresAt: new Date(data.exp * 1000), createdAt: new Date(), }, { upsert: true } ); }, async saveRefresh(sessionId, data) { await db.collection("refreshTokens").replaceOne( { sessionId }, { sessionId, data, expiresAt: new Date(data.exp * 1000), createdAt: new Date(), }, { upsert: true } ); }, async isSessionRevoked(sessionId) { const session = await db.collection("sessions").findOne({ sessionId, expiresAt: { $gt: new Date() }, }); return !session; }, async isRefreshRevoked(sessionId) { const refresh = await db.collection("refreshTokens").findOne({ sessionId, expiresAt: { $gt: new Date() }, }); return !refresh; }, async revokeRefresh(sessionId) { await db.collection("refreshTokens").deleteOne({ sessionId }); }, async revokeSession(sessionId) { await Promise.all([ db.collection("sessions").deleteOne({ sessionId }), db.collection("refreshTokens").deleteOne({ sessionId }), ]); }, }; const toolkit = createSecureTokenToolkit({ encKey: process.env.TOKEN_ENC_KEY, hmacKey: process.env.TOKEN_HMAC_KEY, store: mongoStore, }); ``` ## API Reference ### createToken(payload, options) Creates a new encrypted and signed token. **Parameters:** - `payload` (Object): The data to include in the token - `options` (Object): Token creation options - `expiresIn` (Number): Token expiration time in seconds (default: 900) - `refresh` (Boolean): Whether to create a refresh token (default: false) - `refreshExpiresIn` (Number): Refresh token expiration in seconds (default: 2592000) - `bindIp` (Boolean): Bind token to IP address - `ip` (String): IP address to bind to - `bindFingerprint` (Boolean): Bind token to browser fingerprint - `fingerprint` (String): Browser fingerprint to bind to **Returns:** `{ accessToken, refreshToken }` ```javascript const tokens = await toolkit.createToken( { userId: 123, email: "user@example.com" }, { expiresIn: 3600, refresh: true, bindIp: true, ip: "192.168.1.1", } ); ``` ### verifyToken(token, options) Verifies and decrypts a token. **Parameters:** - `token` (String): The token to verify - `options` (Object): Verification options - `checkIp` (Boolean): Whether to check IP binding - `ip` (String): Current IP address - `checkFingerprint` (Boolean): Whether to check fingerprint binding - `fingerprint` (String): Current browser fingerprint - `ua` (String): User agent for honeypot tracking **Returns:** Decrypted payload object ```javascript try { const payload = await toolkit.verifyToken(token, { checkIp: true, ip: req.ip, ua: req.get("User-Agent"), }); console.log("Token valid:", payload); } catch (error) { console.error("Invalid token:", error.message); } ``` ### refreshToken(refreshToken, options) Creates a new access token using a refresh token. **Parameters:** - `refreshToken` (String): The refresh token - `options` (Object): Same options as verifyToken **Returns:** New token pair `{ accessToken, refreshToken }` ```javascript const newTokens = await toolkit.refreshToken(oldRefreshToken, { ip: req.ip, ua: req.get("User-Agent"), }); ``` ## Security Features ### Honeypot Protection The honeypot system protects against brute force attacks by: 1. Tracking failed verification attempts by IP and User-Agent 2. When the failure threshold is reached, returning decoy tokens instead of errors 3. Logging when decoy tokens are used, indicating a potential attacker ### Session Management Optional session storage allows you to: - Track active sessions - Revoke sessions remotely - Implement logout functionality - Monitor token usage ### Token Binding Bind tokens to specific contexts: - **IP Binding**: Tokens only work from the original IP address - **Fingerprint Binding**: Tokens tied to browser fingerprints ## Best Practices 1. **Key Generation**: Use cryptographically secure random keys: ```javascript const crypto = require("crypto"); const encKey = crypto.randomBytes(32).toString("hex"); const hmacKey = crypto.randomBytes(32).toString("hex"); ``` 2. **Environment Variables**: Store keys securely: ```javascript const config = { encKey: process.env.TOKEN_ENC_KEY, hmacKey: process.env.TOKEN_HMAC_KEY, }; ``` 3. **Short Expiration**: Use short-lived access tokens (15 minutes or less) 4. **Secure Storage**: Implement proper session storage with your database 5. **HTTPS Only**: Always use HTTPS in production 6. **Rate Limiting**: Implement additional rate limiting at the application level ## Error Handling The toolkit throws specific errors for different failure scenarios: - `"Invalid token"`: Token is malformed or signature verification failed - `"Expired"`: Token has passed its expiration time - `"IP mismatch"`: Token IP binding doesn't match current IP - `"Fingerprint mismatch"`: Token fingerprint binding doesn't match - `"Revoked session"`: Session has been revoked - `"Revoked refresh"`: Refresh token has been revoked - `"Not a refresh token"`: Attempted to refresh with an access token ## License MIT ## Links - **Website**: https://compyai.xyz ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. ## Security If you discover a security vulnerability, please send an email to harsh@compyai.xyz. All security vulnerabilities will be promptly addressed.