Skip to content

Commit e8fecd6

Browse files
committed
feat(figerprinting): add test coverage, improve fingerprinting, and update README
1 parent f524a17 commit e8fecd6

File tree

17 files changed

+452
-1395
lines changed

17 files changed

+452
-1395
lines changed

README.md

Lines changed: 184 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
# Tokenly 🔐
22

3+
[![Github Workflow](https://github.com/nekzus/tokenly/actions/workflows/publish.yml/badge.svg?event=push)](https://github.com/Nekzus/tokenly/actions/workflows/publish.yml)
4+
[![npm-version](https://img.shields.io/npm/v/@nekzus/tokenly.svg)](https://www.npmjs.com/package/@nekzus/tokenly)
5+
[![npm-month](https://img.shields.io/npm/dm/@nekzus/tokenly.svg)](https://www.npmjs.com/package/@nekzus/tokenly)
6+
[![npm-total](https://img.shields.io/npm/dt/@nekzus/tokenly.svg?style=flat)](https://www.npmjs.com/package/@nekzus/tokenly)
7+
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
38
<div align="center">
49

5-
[![npm version](https://badge.fury.io/js/@nekzus%2Ftokenly.svg)](https://www.npmjs.com/package/@nekzus/tokenly)
6-
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10+
**Advanced JWT Token Management with Device Fingerprinting**
711

8-
**Secure JWT token management with advanced device fingerprinting**
9-
10-
_Security by default, enhanced with device fingerprinting_
12+
_Enterprise-grade security by default for modern applications_
1113

1214
</div>
1315

16+
## Contents
17+
18+
- [Features](#features)
19+
- [Installation](#installation)
20+
- [Quick Start](#quick-start)
21+
- [Configuration](#configuration)
22+
- [Security Features](#security-features)
23+
- [API Reference](#api-reference)
24+
- [Environment Variables](#environment-variables--secrets)
25+
- [Best Practices](#best-practices)
26+
- [Contributing](#contributing)
27+
- [License](#license)
28+
1429
## ✨ Features
1530

16-
- **🛡️ Security by Default**: JWT tokens with built-in security features
17-
- **🔒 Device Control**: Advanced fingerprinting system for device tracking
18-
- **🚀 Easy Integration**: Simple API that works seamlessly with Express
19-
- **⚡ Performance**: Optimized token generation and validation
20-
- **🛠️ Configurable**: Flexible settings to match your security needs
31+
- **Zero Configuration Required**: Works out of the box with secure defaults
32+
- **Device Fingerprinting**: Unique identification of devices to prevent token theft
33+
- **Framework Agnostic**: Use with Express, Fastify, Koa, or any Node.js framework
34+
- **TypeScript First**: Full type safety and excellent IDE support
35+
- **Production Ready**: Built for enterprise applications
2136

2237
## 📦 Installation
2338

@@ -28,140 +43,204 @@ npm install @nekzus/tokenly
2843
## 🚀 Quick Start
2944

3045
```typescript
31-
import { Tokenly } from '@nekzus/tokenly';
46+
import { Tokenly, getClientIP } from '@nekzus/tokenly';
47+
import dotenv from 'dotenv';
48+
49+
// Load environment variables
50+
dotenv.config();
3251

33-
// Initialize with fingerprinting enabled
52+
// Initialize Tokenly
3453
const auth = new Tokenly({
35-
accessTokenExpiry: '15m',
36-
securityConfig: {
37-
enableFingerprint: true,
38-
maxDevices: 5
39-
}
54+
accessTokenExpiry: '15m',
55+
refreshTokenExpiry: '7d',
56+
securityConfig: {
57+
enableFingerprint: true,
58+
maxDevices: 5
59+
}
4060
});
4161

42-
// Generate a token with device fingerprinting
43-
const token = auth.generateAccessToken(
44-
{ userId: '123' },
45-
undefined,
46-
{
47-
userAgent: req.headers['user-agent'] || '',
48-
ip: getClientIP(req)
62+
// Generate token with fingerprinting
63+
app.post('/login', (req, res) => {
64+
const token = auth.generateAccessToken(
65+
{ userId: '123', role: 'user' },
66+
undefined,
67+
{
68+
userAgent: req.headers['user-agent'] || '',
69+
ip: getClientIP(req.headers)
70+
}
71+
);
72+
res.json({ token });
73+
});
74+
```
75+
76+
## 🔧 Configuration
77+
78+
### Basic Configuration
79+
```typescript
80+
const auth = new Tokenly({
81+
accessTokenExpiry: '15m', // 15 minutes
82+
refreshTokenExpiry: '7d', // 7 days
83+
securityConfig: {
84+
enableFingerprint: true, // Enable device tracking
85+
maxDevices: 5 // Max devices per user
4986
}
50-
);
87+
});
5188
```
5289

53-
## 📘 API Reference
90+
### Advanced Security Configuration
91+
```typescript
92+
const auth = new Tokenly({
93+
accessTokenExpiry: '5m', // Shorter token life
94+
refreshTokenExpiry: '1d', // Daily refresh required
95+
securityConfig: {
96+
enableFingerprint: true, // Required for device tracking
97+
enableBlacklist: true, // Enable token revocation
98+
maxDevices: 3 // Strict device limit
99+
}
100+
});
101+
```
54102

55-
### Configuration
103+
## 🛡️ Security Features
56104

105+
### Device Fingerprinting
106+
- **User Agent**: Browser/client identification
107+
- **IP Address**: Client's IP address
108+
- **Cryptographic Salt**: Unique per instance
109+
- **Consistent Hashing**: Same device = Same fingerprint
110+
111+
### Token Management
112+
- **Access Tokens**: Short-lived JWTs for API access
113+
- **Refresh Tokens**: Long-lived tokens for session maintenance
114+
- **Blacklisting**: Optional token revocation support
115+
- **Expiration Control**: Configurable token lifetimes
116+
117+
### Security Events
57118
```typescript
58-
interface TokenlyConfig {
59-
// Token expiration time (default: '15m')
60-
accessTokenExpiry?: string;
61-
62-
// Security settings
63-
securityConfig?: {
64-
// Enable device fingerprinting (default: false)
65-
enableFingerprint?: boolean;
66-
// Maximum devices per user (default: 5)
67-
maxDevices?: number;
68-
};
69-
}
119+
// Invalid Fingerprint Detection
120+
auth.on('invalid_fingerprint', (event) => {
121+
console.log(`Security Alert: Invalid fingerprint detected`);
122+
console.log(`User: ${event.userId}`);
123+
console.log(`IP: ${event.context.ip}`);
124+
});
125+
126+
// Device Limit Reached
127+
auth.on('max_devices_reached', (event) => {
128+
console.log(`Device limit reached for user: ${event.userId}`);
129+
console.log(`Current devices: ${event.context.currentDevices}`);
130+
});
70131
```
71132

133+
## 📘 API Reference
134+
72135
### Token Generation
136+
```typescript
137+
const token = auth.generateAccessToken(
138+
payload: { userId: string; role: string },
139+
options?: { fingerprint?: string; deviceId?: string },
140+
context?: { userAgent: string; ip: string }
141+
);
142+
```
73143

144+
### IP Detection Helper
74145
```typescript
75-
// Helper for reliable IP detection
76-
function getClientIP(req: express.Request): string {
77-
const forwardedFor = req.headers['x-forwarded-for'];
78-
if (typeof forwardedFor === 'string') {
79-
return forwardedFor.split(',')[0].trim();
80-
}
81-
return req.ip || '';
82-
}
146+
import { getClientIP } from '@nekzus/tokenly';
83147

84-
// Express implementation example
85-
app.post('/login', async (req, res) => {
86-
try {
87-
const token = auth.generateAccessToken(
88-
{ userId: user.id },
89-
undefined,
90-
{
91-
userAgent: req.headers['user-agent'] || '',
92-
ip: getClientIP(req)
93-
}
94-
);
95-
res.json({ token });
96-
} catch (error) {
97-
res.status(400).json({ error: error.message });
98-
}
99-
});
148+
const clientIP = getClientIP(headers, defaultIP);
100149
```
101150

102-
### Token Structure
151+
Priority order:
152+
1. `X-Real-IP`: Direct proxy IP
153+
2. `X-Forwarded-For`: First IP in proxy chain
154+
3. Default IP (if provided)
155+
4. Empty string (fallback)
103156

157+
### Type Definitions
104158
```typescript
105159
interface AccessToken {
106-
// JWT token string
107160
raw: string;
108-
109-
// Token payload
110161
payload: {
111162
userId: string;
112-
fingerprint?: string; // Present when fingerprinting is enabled
113-
aud: string; // 'tokenly-client'
114-
iss: string; // 'tokenly-auth'
115-
iat: string; // Issue timestamp
116-
exp: string; // Expiration timestamp
163+
role: string;
164+
[key: string]: any;
117165
};
118166
}
119-
```
120167

121-
## 🔒 Security Best Practices
168+
interface InvalidFingerprintEvent {
169+
type: 'invalid_fingerprint';
170+
userId: string;
171+
token: string;
172+
context: {
173+
expectedFingerprint: string;
174+
receivedFingerprint: string;
175+
ip: string;
176+
userAgent: string;
177+
timestamp: string;
178+
};
179+
}
122180

123-
### Device Fingerprinting
124-
- **Unique Identification**: Each device is uniquely identified by its IP and User-Agent combination
125-
- **Consistent Tracking**: Same device will generate the same fingerprint across sessions
126-
- **Fraud Prevention**: Helps detect and prevent unauthorized access attempts
181+
interface MaxDevicesEvent {
182+
type: 'max_devices_reached';
183+
userId: string;
184+
context: {
185+
currentDevices: number;
186+
maxAllowed: number;
187+
ip: string;
188+
userAgent: string;
189+
timestamp: string;
190+
};
191+
}
192+
```
127193

128-
### IP Detection
129-
- Always handle `X-Forwarded-For` headers properly in proxy environments
130-
- Use the first IP in the chain as it represents the original client
131-
- Implement appropriate fallbacks for direct connections
194+
## 🔑 Environment Variables & Secrets
132195

133-
### User Agent Handling
134-
- Use complete User-Agent strings for maximum accuracy
135-
- Provide fallbacks for empty values
136-
- Maintain original User-Agent format
196+
### Required Variables
197+
```env
198+
# .env
199+
JWT_SECRET_ACCESS=your_secure_access_token_secret
200+
JWT_SECRET_REFRESH=your_secure_refresh_token_secret
201+
```
137202

138-
## 📚 Examples
203+
When environment variables are not provided, Tokenly automatically:
204+
- Generates cryptographically secure random secrets
205+
- Uses SHA-256 for secret generation
206+
- Implements secure entropy sources
207+
- Creates unique secrets per instance
139208

140-
### Basic Implementation
141-
```typescript
142-
const auth = new Tokenly();
209+
> ⚠️ **Important**: While auto-generated secrets are cryptographically secure, they regenerate on each application restart. This means all previously issued tokens will become invalid. For production environments, always provide permanent secrets through environment variables.
143210
144-
// Generate token
145-
const token = auth.generateAccessToken({ userId: '123' });
211+
### Secret Generation
212+
```bash
213+
# Generate secure random secrets
214+
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
146215
```
147216

148-
### With Fingerprinting
149-
```typescript
150-
const auth = new Tokenly({
151-
securityConfig: { enableFingerprint: true }
152-
});
217+
### Security Guidelines
218+
- Never commit secrets to version control
219+
- Use different secrets for development and production
220+
- Minimum length of 32 characters recommended
221+
- Rotate secrets periodically in production
222+
- Use secret management services when available
153223

154-
// Generate token with device tracking
155-
const token = auth.generateAccessToken(
156-
{ userId: '123' },
157-
undefined,
158-
{ userAgent, ip }
159-
);
160-
```
224+
## 🔐 Best Practices
225+
226+
### Token Security
227+
- Use short-lived access tokens (5-15 minutes)
228+
- Implement refresh token rotation
229+
- Enable blacklisting for critical applications
230+
231+
### Device Management
232+
- Enable fingerprinting for sensitive applications
233+
- Set reasonable device limits per user
234+
- Monitor security events
235+
236+
### IP Detection
237+
- Configure proxy headers correctly
238+
- Use `X-Real-IP` for single proxy setups
239+
- Handle `X-Forwarded-For` for proxy chains
161240

162241
## 🤝 Contributing
163242

164-
Contributions are welcome! Please feel free to submit a Pull Request.
243+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
165244

166245
## 📄 License
167246

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"enabled": true
55
},
66
"files": {
7-
"include": ["src/**/*.{ts,tsx,js,jsx,json}"],
7+
"include": ["src/**/*.{ts,tsx,js,jsx,json}", "tests/**/*.{ts,tsx,js,jsx,json}"],
88
"ignore": ["node_modules", "dist", "examples"]
99
},
1010
"linter": {

dist/index.cjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

dist/index.cjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/types/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { Tokenly } from './tokenManager.js';
2-
export type { AccessToken, TokenlyConfig } from './types.js';
2+
export type { AccessToken, Headers, InvalidFingerprintEvent, MaxDevicesEvent, TokenContext, TokenlyConfig } from './types.js';
33
export { getClientIP } from './utils/ipHelper.js';

dist/types/tokenManager.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,13 @@ export declare class Tokenly {
6868
private eventListeners;
6969
private autoRotationInterval;
7070
private fingerprintCache;
71-
private readonly instanceSalt;
71+
private readonly instanceId;
7272
/**
7373
* Initialize Tokenly with custom configuration
7474
* @param config Optional configuration for token management
7575
*/
7676
constructor(config?: TokenlyConfig);
77+
private generateSecret;
7778
/**
7879
* Format Unix timestamp to ISO date string
7980
* @param timestamp Unix timestamp in seconds

dist/types/utils/ipHelper.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { Headers } from '../types.js';
12
/**
23
* Helper function to get the real client IP from various headers
34
* @param headers Object containing HTTP headers
5+
* @param defaultIP Optional default IP if no headers found
46
* @returns string Client IP address
57
*/
6-
export declare function getClientIP(headers: Record<string, string | string[] | undefined>, defaultIP?: string): string;
8+
export declare function getClientIP(headers: Headers, defaultIP?: string): string;

0 commit comments

Comments
 (0)