@compyai/securetoken
v1.5.1
Published
A modular, encrypted, DB-agnostic token system with optional refresh support, honeypot protection, and revocation. A secure alternative to JWT.
Maintainers
Keywords
Readme
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
npm install @compyai/securetokenQuick Start
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
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:#fff3e0Configuration
Basic Configuration
const config = {
encKey: "your-256-bit-encryption-key-in-hex",
hmacKey: "your-256-bit-hmac-key-in-hex",
};Advanced Configuration with Honeypot
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)
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)
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)
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
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 tokenoptions(Object): Token creation optionsexpiresIn(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 addressip(String): IP address to bind tobindFingerprint(Boolean): Bind token to browser fingerprintfingerprint(String): Browser fingerprint to bind to
Returns: { accessToken, refreshToken }
const tokens = await toolkit.createToken(
{ userId: 123, email: "[email protected]" },
{
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 verifyoptions(Object): Verification optionscheckIp(Boolean): Whether to check IP bindingip(String): Current IP addresscheckFingerprint(Boolean): Whether to check fingerprint bindingfingerprint(String): Current browser fingerprintua(String): User agent for honeypot tracking
Returns: Decrypted payload object
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 tokenoptions(Object): Same options as verifyToken
Returns: New token pair { accessToken, refreshToken }
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:
- Tracking failed verification attempts by IP and User-Agent
- When the failure threshold is reached, returning decoy tokens instead of errors
- 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
Key Generation: Use cryptographically secure random keys:
const crypto = require("crypto"); const encKey = crypto.randomBytes(32).toString("hex"); const hmacKey = crypto.randomBytes(32).toString("hex");Environment Variables: Store keys securely:
const config = { encKey: process.env.TOKEN_ENC_KEY, hmacKey: process.env.TOKEN_HMAC_KEY, };Short Expiration: Use short-lived access tokens (15 minutes or less)
Secure Storage: Implement proper session storage with your database
HTTPS Only: Always use HTTPS in production
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 [email protected]. All security vulnerabilities will be promptly addressed.
