Skip to content

Commit fc590a8

Browse files
committed
feat(security): improve data validation & required secret key 🔒
- add cache size validation (max 10k tokens) - add time expiration limit (max 1 year) - enhance error handling and test coverage - fix architecture diagram (key preparation vs derivation) - make secret parameter required (8-255 chars) - update JWTOptions interface and documentation
1 parent f683b93 commit fc590a8

File tree

15 files changed

+699
-176
lines changed

15 files changed

+699
-176
lines changed

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
---
99

10+
## [1.2.0] - 2025-09-06
11+
12+
### Breaking Changes
13+
- `secret` parameter is now required in constructor
14+
- `JWTOptions.secret` changed from optional (`secret?: string`) to required (`secret: string`)
15+
16+
### Security
17+
- Added cache size validation (max 10,000 tokens)
18+
- Added time expiration limit (max 1 year)
19+
- Made secret parameter required for enhanced security
20+
- Enhanced secret key validation (8-255 characters)
21+
22+
### Added
23+
- Cache size validation with configurable limits
24+
- Time expiration validation with 1-year maximum
25+
- Enhanced error messages for better debugging
26+
- Comprehensive test coverage for new validations
27+
28+
### Changed
29+
- Updated README documentation to reflect required secret parameter
30+
- Fixed architecture diagram (Key Preparation vs Key Derivation)
31+
- Simplified JSDoc comments across multiple files
32+
- Enhanced error handling and validation coverage
33+
34+
### Fixed
35+
- Test suite updated to handle required secret parameter
36+
- Architecture diagram accuracy improved
37+
- Documentation consistency across all files
38+
39+
---
40+
1041
## [1.1.1] - 2025-09-06
1142

1243
### Security

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
![npm version](https://img.shields.io/npm/v/@neabyte/secure-jwt)
44
![node version](https://img.shields.io/node/v/@neabyte/secure-jwt)
55
![typescript version](https://img.shields.io/badge/typeScript-5.9.2-blue.svg)
6-
![coverage](https://img.shields.io/badge/coverage-99.08%25-brightgreen)
6+
![coverage](https://img.shields.io/badge/coverage-98%25-brightgreen)
77
![license](https://img.shields.io/npm/l/@neabyte/secure-jwt.svg)
88

99
A secure JWT implementation with AES-256-GCM encryption for Node.js applications.
@@ -109,7 +109,7 @@ const arrayToken: string = jwt.sign([1, 2, 3])
109109

110110
```javascript
111111
const jwt = new SecureJWT({
112-
secret: 'your-secret-key', // Required: 8+ characters
112+
secret: 'your-secret-key', // Required: 8-255 characters
113113
expireIn: '1h', // Required: Time string
114114
version: '1.0.0', // Optional: Default '1.0.0'
115115
cached: 1000 // Optional: Cache size (default: 1000)
@@ -140,8 +140,8 @@ new SecureJWT(options: JWTOptions)
140140
```
141141

142142
**Options:**
143-
- `secret?: string` - Secret key (8+ chars, optional)
144-
- `expireIn: string` - Token expiration time
143+
- `secret: string` - Secret key (8-255 chars, required for security)
144+
- `expireIn: string` - Token expiration time (required for security)
145145
- `version?: string` - Token version (default: '1.0.0')
146146
- `cached?: number` - Cache size for performance (default: 1000)
147147

@@ -201,17 +201,18 @@ graph TD
201201
F --> G[Create Token Structure]
202202
G --> H[Base64 Encode]
203203
H --> I[Secure JWT Token]
204-
205-
J[Secret Key] --> K[Key Derivation]
204+
205+
J[Secret Key] --> K[Key Preparation]
206206
K --> F
207207
L[Random Salt] --> K
208-
209-
M[Version] --> N[Additional Authenticated Data]
208+
209+
M[Version] --> N[Additional
210+
Authenticated Data]
210211
N --> F
211-
212+
212213
F --> O[Authentication Tag]
213214
O --> G
214-
215+
215216
style A fill:#e1f5fe,color:#000
216217
style I fill:#c8e6c9,color:#000
217218
style F fill:#fff3e0,color:#000
@@ -227,12 +228,12 @@ graph LR
227228
C --> D[Integrity Layer]
228229
D --> E[Encoding Layer]
229230
E --> F[Secure Token]
230-
231+
231232
G[Secret Key] --> C
232233
H[Random IV] --> C
233234
I[Version AAD] --> C
234235
J[Auth Tag] --> D
235-
236+
236237
style B fill:#ffebee,color:#000
237238
style C fill:#fff3e0,color:#000
238239
style D fill:#f3e5f5,color:#000

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@neabyte/secure-jwt",
33
"description": "Secure JWT with AES-256-GCM encryption, built-in caching, tamper detection, and TypeScript support",
4-
"version": "1.1.1",
4+
"version": "1.2.0",
55
"type": "module",
66
"main": "./dist/index.js",
77
"types": "./dist/index.d.ts",

src/index.ts

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import {
1616
} from '@utils/index'
1717

1818
/**
19-
* Secure JWT implementation with encryption support
20-
* Handles token creation, verification, and data extraction
19+
* JWT implementation with encryption
20+
* Creates, verifies, and extracts data from tokens
2121
*/
2222
export default class SecureJWT {
2323
/** Secret key for encryption */
@@ -33,17 +33,18 @@ export default class SecureJWT {
3333

3434
/**
3535
* Creates a new SecureJWT instance
36-
* @param options - The configuration for token expiration, secret key, and version
36+
* @param options - Configuration for token expiration, secret key, and version
3737
*/
3838
constructor(options: JWTOptions) {
3939
ErrorHandler.validateOptions(options)
4040
ErrorHandler.validateExpireIn(options.expireIn)
41-
if (options.secret !== undefined) {
42-
ErrorHandler.validateSecret(options.secret)
43-
}
41+
ErrorHandler.validateSecret(options.secret)
4442
if (options.version !== undefined) {
4543
ErrorHandler.validateVersion(options.version)
4644
}
45+
if (options.cached !== undefined) {
46+
ErrorHandler.validateCacheSize(options.cached)
47+
}
4748
this.#secret = this.generateSecret(options.secret)
4849
this.#expireInMs = parsetimeToMs(options.expireIn)
4950
this.#version = options.version ?? '1.0.0'
@@ -52,22 +53,19 @@ export default class SecureJWT {
5253
}
5354

5455
/**
55-
* Creates a secret key from the provided secret or generates a random one
56-
* @param secret - The optional secret string for key creation
57-
* @returns The Buffer containing the secret key
56+
* Creates a secret key from the provided secret
57+
* @param secret - Secret string for key creation
58+
* @returns Buffer containing the secret key
5859
*/
59-
private generateSecret(secret?: string): Buffer {
60-
if (secret != null && secret.length > 0) {
61-
const salt = randomBytes(32)
62-
return Buffer.concat([salt, Buffer.from(secret, 'utf8')])
63-
}
64-
return randomBytes(32)
60+
private generateSecret(secret: string): Buffer {
61+
const salt = randomBytes(32)
62+
return Buffer.concat([salt, Buffer.from(secret, 'utf8')])
6563
}
6664

6765
/**
6866
* Encrypts data using AES-256-GCM encryption
69-
* @param data - The string data to encrypt
70-
* @returns The object with encrypted data, initialization vector, and authentication tag
67+
* @param data - String data to encrypt
68+
* @returns Object with encrypted data, initialization vector, and authentication tag
7169
*/
7270
private encrypt(data: string): TokenEncrypted {
7371
ErrorHandler.validateEncryptionData(data)
@@ -89,8 +87,8 @@ export default class SecureJWT {
8987

9088
/**
9189
* Decrypts data using AES-256-GCM decryption
92-
* @param tokenEncrypted - The object with encrypted data, initialization vector, and authentication tag
93-
* @returns The decrypted data as string
90+
* @param tokenEncrypted - Object with encrypted data, initialization vector, and authentication tag
91+
* @returns Decrypted data as string
9492
*/
9593
private decrypt(tokenEncrypted: TokenEncrypted): string {
9694
try {
@@ -116,9 +114,9 @@ export default class SecureJWT {
116114
}
117115

118116
/**
119-
* Creates a secure JWT token from data
120-
* @param data - The data to sign (will be JSON stringified)
121-
* @returns The JWT token string
117+
* Creates a JWT token from data
118+
* @param data - Data to sign (will be JSON stringified)
119+
* @returns JWT token string
122120
*/
123121
sign(data: unknown): string {
124122
try {
@@ -162,8 +160,8 @@ export default class SecureJWT {
162160

163161
/**
164162
* Validates if a JWT token is valid
165-
* @param token - The Base64 encoded token to check
166-
* @returns The boolean indicating if token is valid and not expired
163+
* @param token - Base64 encoded token to check
164+
* @returns Boolean indicating if token is valid and not expired
167165
*/
168166
verify(token: string): boolean {
169167
try {
@@ -217,7 +215,7 @@ export default class SecureJWT {
217215

218216
/**
219217
* Validates a JWT token and throws specific errors
220-
* @param token - The Base64 encoded token to validate
218+
* @param token - Base64 encoded token to validate
221219
* @throws {ValidationError} When token format is invalid
222220
* @throws {TokenExpiredError} When token has expired
223221
* @throws {DecryptionError} When token decryption fails
@@ -260,8 +258,8 @@ export default class SecureJWT {
260258

261259
/**
262260
* Extracts data from a JWT token
263-
* @param token - The Base64 encoded token to decode
264-
* @returns The decoded payload data
261+
* @param token - Base64 encoded token to decode
262+
* @returns Decoded payload data
265263
* @throws {ValidationError} When token format is invalid
266264
* @throws {TokenExpiredError} When token has expired
267265
* @throws {DecryptionError} When token decryption fails

src/interfaces/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* JWT payload structure
33
*/
44
export interface JWTPayload {
5-
/** The actual data payload */
5+
/** Actual data payload */
66
payload: unknown
77
/** Expiration timestamp in seconds */
88
exp: number
@@ -14,10 +14,10 @@ export interface JWTPayload {
1414
* Configuration for JWT operations
1515
*/
1616
export interface JWTOptions {
17+
/** Secret key for signing tokens (required for security) */
18+
secret: string
1719
/** Token expiration time as a string (e.g., '1h', '30m', '7d') */
1820
expireIn: string
19-
/** Secret key for signing tokens (optional) */
20-
secret?: string
2121
/** Version identifier for the token (optional) */
2222
version?: string
2323
/** Cache configuration (optional) */
@@ -28,7 +28,7 @@ export interface JWTOptions {
2828
* Time value with its unit
2929
*/
3030
export interface TimeUnit {
31-
/** Numeric value of the time */
31+
/** Numeric value of time */
3232
value: number
3333
/** Time unit (milliseconds, seconds, minutes, hours, days, months, years) */
3434
unit: 'ms' | 's' | 'm' | 'h' | 'd' | 'M' | 'y'
@@ -68,7 +68,7 @@ export interface TokenEncrypted {
6868
* Payload data structure with timing and version information
6969
*/
7070
export interface PayloadData {
71-
/** The actual payload data */
71+
/** Actual payload data */
7272
data: unknown
7373
/** Expiration timestamp in seconds */
7474
exp: number
@@ -82,7 +82,7 @@ export interface PayloadData {
8282
* Cache entry structure with expiration and usage tracking
8383
*/
8484
export interface CacheEntry<T> {
85-
/** The stored data */
85+
/** Stored data */
8686
data: T
8787
/** When this entry expires (milliseconds since epoch) */
8888
expiresAt: number

src/utils/Cache.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CacheEntry } from '@interfaces/index'
22

33
/**
4-
* In-memory cache that stores data with expiration times
4+
* In-memory cache that stores data with expiration
55
*/
66
export class Cache<T> {
77
private readonly cache = new Map<string, CacheEntry<T>>()
@@ -20,8 +20,8 @@ export class Cache<T> {
2020

2121
/**
2222
* Gets a value from the cache
23-
* @param key - The key to look up
24-
* @returns The cached value or undefined if not found or expired
23+
* @param key - Key to look up
24+
* @returns Cached value or undefined if not found or expired
2525
*/
2626
get(key: string): T | undefined {
2727
const entry = this.cache.get(key)
@@ -38,8 +38,8 @@ export class Cache<T> {
3838

3939
/**
4040
* Stores a value in the cache
41-
* @param key - The key to store under
42-
* @param value - The value to store
41+
* @param key - Key to store under
42+
* @param value - Value to store
4343
* @param ttl - Expiration time in milliseconds (optional, uses default if not provided, minimum: 1ms)
4444
*/
4545
set(key: string, value: T, ttl?: number): void {
@@ -61,7 +61,7 @@ export class Cache<T> {
6161

6262
/**
6363
* Checks if a key exists in the cache and is not expired
64-
* @param key - The key to check
64+
* @param key - Key to check
6565
* @returns True if the key exists and is valid
6666
*/
6767
has(key: string): boolean {

0 commit comments

Comments
 (0)