Skip to content

Commit 32766bc

Browse files
committed
fix(validation): improve constructor validation and error handling 🔧
- Add algorithm parameter validation - Fix validation order to prevent masked error messages - Fix whitespace handling in expireIn parameter - Improve base64 string validation - Increase test coverage to 98.37%
1 parent de22afd commit 32766bc

File tree

9 files changed

+160
-14
lines changed

9 files changed

+160
-14
lines changed

CHANGELOG.md

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

88
---
99

10+
## [1.4.5] - 2025-09-06
11+
12+
### Fixed
13+
- Added validation for encryption algorithm parameter
14+
- Base64 string validation in token processing
15+
- Constructor validation order to prevent masked error messages
16+
- Test coverage increased from 98.32% to 98.37%
17+
- Whitespace handling in `expireIn` parameter (e.g., " 1h " now works)
18+
19+
### Changed
20+
- Error message specificity and validation order
21+
- Parameter validation across all constructor options
22+
- Added tests for previously uncovered validation paths
23+
24+
### Technical
25+
- All options now validated before processing to surface specific errors
26+
- Added `.trim()` to handle whitespace in time strings
27+
- Added `validateAlgorithm` method with proper error messages
28+
29+
---
30+
1031
## [1.4.4] - 2025-09-06
1132

1233
### Changed

README.md

Lines changed: 1 addition & 1 deletion
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-98.32%25-brightgreen)
6+
![coverage](https://img.shields.io/badge/coverage-98.37%25-brightgreen)
77
![license](https://img.shields.io/npm/l/@neabyte/secure-jwt.svg)
88

99
A secure JWT library implementation with multiple encryption algorithms, zero dependencies, and built-in security for Node.js applications. Designed for high performance and reliability with TypeScript support.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@neabyte/secure-jwt",
3-
"description": "Secure JWT with AES-256-GCM & ChaCha20-Poly1305 encryption, built-in caching, tamper detection, and TypeScript support",
4-
"version": "1.4.4",
3+
"description": "A secure JWT library with multiple encryption algorithms, zero dependencies, and built-in security for Node.js applications.",
4+
"version": "1.4.5",
55
"type": "module",
66
"main": "./dist/index.js",
77
"types": "./dist/index.d.ts",

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ export default class SecureJWT {
4949
ErrorHandler.validateOptions(options)
5050
ErrorHandler.validateExpireIn(options.expireIn)
5151
ErrorHandler.validateSecret(options.secret)
52+
if (options.algorithm !== undefined) {
53+
ErrorHandler.validateAlgorithm(options.algorithm)
54+
}
5255
if (options.keyDerivation !== undefined) {
5356
ErrorHandler.validateKeyDerivation(options.keyDerivation)
5457
}

src/utils/ErrorHandler.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
VersionMismatchError,
1111
getErrorMessage
1212
} from '@utils/index'
13-
import type { KeyDerivationAlgo } from '@interfaces/index'
13+
import type { EncryptionAlgo, KeyDerivationAlgo } from '@interfaces/index'
1414

1515
/**
1616
* Handles errors and validates data for JWT operations
@@ -185,6 +185,24 @@ export class ErrorHandler {
185185
}
186186
}
187187

188+
/**
189+
* Checks if encryption algorithm is valid
190+
* @param algorithm - Encryption algorithm to check
191+
* @throws {ValidationError} When algorithm is invalid
192+
*/
193+
static validateAlgorithm(algorithm: string): void {
194+
if (typeof algorithm !== 'string') {
195+
throw new ValidationError(getErrorMessage('ALGORITHM_MUST_BE_STRING'))
196+
}
197+
if (algorithm.length === 0) {
198+
throw new ValidationError(getErrorMessage('ALGORITHM_CANNOT_BE_EMPTY'))
199+
}
200+
const validAlgorithms = ['aes-256-gcm', 'chacha20-poly1305'] as const
201+
if (!validAlgorithms.includes(algorithm as EncryptionAlgo)) {
202+
throw new ValidationError(getErrorMessage('INVALID_ALGORITHM'))
203+
}
204+
}
205+
188206
/**
189207
* Checks if payload size is within limits
190208
* @param payload - Payload string to check
@@ -377,6 +395,9 @@ export class ErrorHandler {
377395
* @throws {ValidationError} When base64 decoding fails
378396
*/
379397
static validateBase64Decode(token: string, errorMessage: string): string {
398+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(token)) {
399+
throw new ValidationError(errorMessage)
400+
}
380401
try {
381402
return Buffer.from(token, 'base64').toString('utf8')
382403
} catch {

src/utils/ErrorMap.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ export const errorMessages = {
8787
KEY_DERIVATION_INVALID_METHOD: 'Invalid key derivation method. Must be "basic" or "pbkdf2"',
8888
INVALID_KEY_DERIVATION_METHOD: 'Invalid key derivation method provided',
8989

90+
// Algorithm errors
91+
ALGORITHM_MUST_BE_STRING: 'Algorithm must be a string',
92+
ALGORITHM_CANNOT_BE_EMPTY: 'Algorithm cannot be empty',
93+
9094
// Options errors
9195
OPTIONS_MUST_BE_OBJECT: 'Options must be an object',
9296
EXPIRE_IN_REQUIRED: 'expireIn is required and must be a string',

src/utils/TimeParser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function parseTimeString(timeString: string): TimeUnit {
5757
*/
5858
export function parsetimeToMs(timeString: string): number {
5959
ErrorHandler.validateTimeString(timeString)
60-
const timeUnit = parseTimeString(timeString)
60+
const timeUnit = parseTimeString(timeString.trim())
6161
const milliseconds = timeToMs(timeUnit)
6262
const maxExpirationMs = 365 * 24 * 60 * 60 * 1000
6363
if (milliseconds > maxExpirationMs) {

tests/ErrorHandler.test.ts

Lines changed: 105 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ describe('ErrorHandler', () => {
4848
const wrapped = ErrorHandler.wrap(fn)
4949
expect(() => wrapped('test')).toThrow('Unknown error occurred')
5050
})
51+
52+
it('should create SecureJWTError with error message for Error objects', () => {
53+
const fn = jest.fn().mockImplementation(() => {
54+
throw new Error('Custom error message')
55+
})
56+
const wrapped = ErrorHandler.wrap(fn)
57+
expect(() => wrapped('test')).toThrow('Custom error message')
58+
})
5159
})
5260

5361
describe('validateData', () => {
@@ -454,10 +462,8 @@ describe('ErrorHandler', () => {
454462
expect(result).toBe('test')
455463
})
456464

457-
it('should not throw for invalid base64 (Buffer.from handles it)', () => {
458-
// Buffer.from doesn't throw for invalid base64, it just produces garbage
459-
const result = ErrorHandler.validateBase64Decode('invalid base64!', 'Invalid base64')
460-
expect(typeof result).toBe('string')
465+
it('should throw ValidationError for invalid base64', () => {
466+
expect(() => ErrorHandler.validateBase64Decode('invalid base64!', 'Invalid base64')).toThrow(ValidationError)
461467
})
462468
})
463469

@@ -619,10 +625,8 @@ describe('ErrorHandler', () => {
619625
expect(result).toBe(original)
620626
})
621627

622-
it('should not throw for invalid base64 (Buffer.from handles it)', () => {
623-
// Buffer.from doesn't throw for invalid base64, it just produces garbage
624-
const result = ErrorHandler.validateBase64Decode('invalid base64!', 'Invalid base64')
625-
expect(typeof result).toBe('string')
628+
it('should throw ValidationError for invalid base64', () => {
629+
expect(() => ErrorHandler.validateBase64Decode('invalid base64!', 'Invalid base64')).toThrow(ValidationError)
626630
})
627631

628632
it('should throw ValidationError when Buffer.from throws', () => {
@@ -1229,4 +1233,97 @@ describe('ErrorHandler', () => {
12291233
}
12301234
})
12311235
})
1236+
1237+
describe('validateAlgorithm', () => {
1238+
it('should not throw for valid algorithms', () => {
1239+
expect(() => ErrorHandler.validateAlgorithm('aes-256-gcm')).not.toThrow()
1240+
expect(() => ErrorHandler.validateAlgorithm('chacha20-poly1305')).not.toThrow()
1241+
})
1242+
1243+
it('should throw ValidationError for non-string algorithm', () => {
1244+
expect(() => ErrorHandler.validateAlgorithm(123 as any)).toThrow(ValidationError)
1245+
expect(() => ErrorHandler.validateAlgorithm({} as any)).toThrow(ValidationError)
1246+
})
1247+
1248+
it('should throw ValidationError for empty string algorithm', () => {
1249+
expect(() => ErrorHandler.validateAlgorithm('')).toThrow(ValidationError)
1250+
})
1251+
1252+
it('should throw ValidationError for invalid algorithm', () => {
1253+
expect(() => ErrorHandler.validateAlgorithm('invalid-algo')).toThrow(ValidationError)
1254+
expect(() => ErrorHandler.validateAlgorithm('aes-128-gcm')).toThrow(ValidationError)
1255+
})
1256+
})
1257+
1258+
describe('validateExpiration', () => {
1259+
it('should not throw for valid expiration', () => {
1260+
const now = Math.floor(Date.now() / 1000)
1261+
const future = now + 3600
1262+
expect(() => ErrorHandler.validateExpiration(future, now + 7200)).not.toThrow()
1263+
})
1264+
1265+
it('should throw ValidationError for expiration too far in future', () => {
1266+
const now = Math.floor(Date.now() / 1000)
1267+
const future = now + 3600
1268+
expect(() => ErrorHandler.validateExpiration(future, now + 1800)).toThrow(ValidationError)
1269+
})
1270+
})
1271+
1272+
describe('validateVersionCompatibility', () => {
1273+
it('should not throw for matching versions', () => {
1274+
expect(() => ErrorHandler.validateVersionCompatibility('1.0.0', '1.0.0')).not.toThrow()
1275+
})
1276+
1277+
it('should throw VersionMismatchError for different versions', () => {
1278+
expect(() => ErrorHandler.validateVersionCompatibility('1.0.0', '2.0.0')).toThrow(VersionMismatchError)
1279+
})
1280+
1281+
it('should throw VersionMismatchError for downgrade attack', () => {
1282+
expect(() => ErrorHandler.validateVersionCompatibility('1.0.0', '2.0.0')).toThrow(VersionMismatchError)
1283+
})
1284+
1285+
it('should throw VersionMismatchError for upgrade not supported', () => {
1286+
expect(() => ErrorHandler.validateVersionCompatibility('2.0.0', '1.0.0')).toThrow(VersionMismatchError)
1287+
})
1288+
})
1289+
1290+
describe('validateTokenTimestamps', () => {
1291+
it('should not throw for matching timestamps', () => {
1292+
const now = Math.floor(Date.now() / 1000)
1293+
expect(() => ErrorHandler.validateTokenTimestamps(now + 3600, now + 3600, now, now)).not.toThrow()
1294+
})
1295+
1296+
it('should throw ValidationError for mismatched timestamps', () => {
1297+
const now = Math.floor(Date.now() / 1000)
1298+
expect(() => ErrorHandler.validateTokenTimestamps(now + 3600, now + 1800, now, now)).toThrow(ValidationError)
1299+
expect(() => ErrorHandler.validateTokenTimestamps(now + 3600, now + 3600, now, now + 1)).toThrow(ValidationError)
1300+
})
1301+
})
1302+
1303+
describe('validateJSONParse', () => {
1304+
it('should parse valid JSON', () => {
1305+
const result = ErrorHandler.validateJSONParse('{"test": "value"}', 'Error')
1306+
expect(result).toEqual({ test: 'value' })
1307+
})
1308+
1309+
it('should throw ValidationError for invalid JSON', () => {
1310+
expect(() => ErrorHandler.validateJSONParse('invalid json', 'Custom error')).toThrow(ValidationError)
1311+
})
1312+
})
1313+
1314+
describe('validateBase64Decode', () => {
1315+
it('should decode valid base64', () => {
1316+
const encoded = Buffer.from('test').toString('base64')
1317+
const result = ErrorHandler.validateBase64Decode(encoded, 'Error')
1318+
expect(result).toBe('test')
1319+
})
1320+
1321+
it('should throw ValidationError for invalid base64', () => {
1322+
expect(() => ErrorHandler.validateBase64Decode('invalid base64!', 'Custom error')).toThrow(ValidationError)
1323+
})
1324+
1325+
it('should throw ValidationError for malformed base64', () => {
1326+
expect(() => ErrorHandler.validateBase64Decode('invalid@#$%', 'Custom error')).toThrow(ValidationError)
1327+
})
1328+
})
12321329
})

tests/ErrorMap.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ describe('ErrorMap', () => {
7676
})
7777

7878
it('should have consistent structure', () => {
79-
expect(Object.keys(errorMessages)).toHaveLength(72)
79+
expect(Object.keys(errorMessages)).toHaveLength(74)
8080
})
8181

8282
it('should have all messages as strings', () => {

0 commit comments

Comments
 (0)