From 5303812bcbf18cc1f12c66cdc51c95b20ecfe7b6 Mon Sep 17 00:00:00 2001 From: larryrider Date: Thu, 9 Jul 2026 10:37:51 +0200 Subject: [PATCH 1/4] fix: update base64 decoding for JWT and its tests --- src/auth/validateJwtAndCheckExpiration.ts | 3 ++- .../validateJwtAndCheckExpiration.test.ts | 24 +++++++++++++++---- .../validateTokenAndCheckExpiration.test.ts | 8 +++---- test/tsconfig.json | 6 +++++ 4 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 test/tsconfig.json diff --git a/src/auth/validateJwtAndCheckExpiration.ts b/src/auth/validateJwtAndCheckExpiration.ts index d5be35f..7ab195e 100644 --- a/src/auth/validateJwtAndCheckExpiration.ts +++ b/src/auth/validateJwtAndCheckExpiration.ts @@ -14,7 +14,8 @@ export default function validateJwtAndCheckExpiration(token: string): DecodedJwt } try { - const payload = JSON.parse(atob(token.split('.')[1])); + const base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); + const payload = JSON.parse(Buffer.from(base64, 'base64').toString()); if (typeof payload.exp !== 'number') { return null; } diff --git a/test/auth/validateJwtAndCheckExpiration.test.ts b/test/auth/validateJwtAndCheckExpiration.test.ts index 2e7a6b8..a75e303 100644 --- a/test/auth/validateJwtAndCheckExpiration.test.ts +++ b/test/auth/validateJwtAndCheckExpiration.test.ts @@ -17,20 +17,20 @@ describe('validateJwtAndCheckExpiration tests', () => { }); test('when the token does not contain an expiration claim, then null is returned', () => { - const payload = btoa(JSON.stringify({ sub: 'user123' })); + const payload = Buffer.from(JSON.stringify({ sub: 'user123' })).toString('base64'); const token = `header.${payload}.signature`; expect(validateJwtAndCheckExpiration(token)).to.be.equal(null); }); test('when the token expiration value is not a number, then null is returned', () => { - const payload = btoa(JSON.stringify({ exp: 'not-a-number' })); + const payload = Buffer.from(JSON.stringify({ exp: 'not-a-number' })).toString('base64'); const token = `header.${payload}.signature`; expect(validateJwtAndCheckExpiration(token)).to.be.equal(null); }); test('when the token has a valid structure with expiration, then the exp and iat claims are returned', () => { const expiration = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now - const payload = btoa(JSON.stringify({ exp: expiration, sub: 'user123' })); + const payload = Buffer.from(JSON.stringify({ exp: expiration, sub: 'user123' })).toString('base64'); const token = `header.${payload}.signature`; expect(validateJwtAndCheckExpiration(token)).to.deep.equal({ exp: expiration, @@ -38,10 +38,26 @@ describe('validateJwtAndCheckExpiration tests', () => { }); }); + test('when the token payload is base64url-encoded with characters outside the standard alphabet, then the claims are returned', () => { + const issuedAt = 1577836800; + const expiration = 4102444800; + // Payload encodes to a segment containing both - and _ (base64url alphabet) + const payload = Buffer.from(JSON.stringify({ exp: expiration, iat: issuedAt, sub: 'WO¥þ=(E' })).toString( + 'base64url', + ); + expect(payload).to.match(/-/); + expect(payload).to.match(/_/); + const token = `header.${payload}.signature`; + expect(validateJwtAndCheckExpiration(token)).to.deep.equal({ + exp: expiration, + iat: issuedAt, + }); + }); + test('when the token has a valid structure with expiration and issued-at, then both claims are returned', () => { const issuedAt = Math.floor(Date.now() / 1000); const expiration = issuedAt + 3600; // 1 hour from now - const payload = btoa(JSON.stringify({ exp: expiration, iat: issuedAt, sub: 'user123' })); + const payload = Buffer.from(JSON.stringify({ exp: expiration, iat: issuedAt, sub: 'user123' })).toString('base64'); const token = `header.${payload}.signature`; expect(validateJwtAndCheckExpiration(token)).to.deep.equal({ exp: expiration, diff --git a/test/auth/validateTokenAndCheckExpiration.test.ts b/test/auth/validateTokenAndCheckExpiration.test.ts index b4a6c98..21714e1 100644 --- a/test/auth/validateTokenAndCheckExpiration.test.ts +++ b/test/auth/validateTokenAndCheckExpiration.test.ts @@ -10,7 +10,7 @@ describe('validateTokenAndCheckExpiration tests', () => { test('when the token is structurally valid but has expired, then EXPIRED is returned', () => { const expiration = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago - const payload = btoa(JSON.stringify({ exp: expiration })); + const payload = Buffer.from(JSON.stringify({ exp: expiration })).toString('base64'); const token = `header.${payload}.signature`; expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.EXPIRED); @@ -18,7 +18,7 @@ describe('validateTokenAndCheckExpiration tests', () => { test('when the token is valid and expires within six hours, then REFRESH_REQUIRED is returned', () => { const expiration = Math.floor(Date.now() / 1000) + 6 * 60 * 60; // 6 hours from now - const payload = btoa(JSON.stringify({ exp: expiration })); + const payload = Buffer.from(JSON.stringify({ exp: expiration })).toString('base64'); const token = `header.${payload}.signature`; expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.REFRESH_REQUIRED); @@ -27,7 +27,7 @@ describe('validateTokenAndCheckExpiration tests', () => { test('when the token is valid, has an issued-at claim, and is past 50% of its lifetime, then REFRESH_REQUIRED is returned', () => { const issuedAt = Math.floor(Date.now() / 1000) - 16 * 60 * 60; // issued 16h ago const expiration = issuedAt + 32 * 60 * 60; // total lifetime 32h, 16h (50%) remaining - const payload = btoa(JSON.stringify({ exp: expiration, iat: issuedAt })); + const payload = Buffer.from(JSON.stringify({ exp: expiration, iat: issuedAt })).toString('base64'); const token = `header.${payload}.signature`; expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.REFRESH_REQUIRED); @@ -35,7 +35,7 @@ describe('validateTokenAndCheckExpiration tests', () => { test('when the token is valid and expires in thirty days, then VALID is returned', () => { const expiration = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; // 30 days from now - const payload = btoa(JSON.stringify({ exp: expiration })); + const payload = Buffer.from(JSON.stringify({ exp: expiration })).toString('base64'); const token = `header.${payload}.signature`; expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.VALID); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..ffac201 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "types": ["node"] + }, + "include": ["./**/*.ts"] +} From 97daeb48991a4723b92c65e23e426cf67050bbfc Mon Sep 17 00:00:00 2001 From: larryrider Date: Thu, 9 Jul 2026 10:38:29 +0200 Subject: [PATCH 2/4] fix: bump version to 1.5.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e62fdde..c1e77b1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@internxt/lib", - "version": "1.5.0", + "version": "1.5.1", "description": "Common logic shared between different projects of Internxt ", "main": "dist/index.js", "types": "dist/index.d.ts", From 1e124c0c4c86fc7c173bdc9a637fbbf620b44c2a Mon Sep 17 00:00:00 2001 From: larryrider Date: Thu, 9 Jul 2026 10:43:28 +0200 Subject: [PATCH 3/4] feat: add README.md with project overview and installation instructions --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7381697 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# @internxt/lib + +Common logic shared between different projects of Internxt. + +A zero-dependency TypeScript library providing utilities for AES-256-GCM encryption, authentication helpers (email/password validation, JWT token handling), file/item naming, and string manipulation. + +## Installation + +```sh +npm install @internxt/lib +``` + +## Development + +```sh +# Install dependencies +npm install + +# Run tests +npm run test + +# Build +npm run build +``` + +## License + +[MIT](LICENSE) From 90bb9fca4dbc386d37c8bb0f3d72a7a9dd50f189 Mon Sep 17 00:00:00 2001 From: larryrider Date: Thu, 9 Jul 2026 10:51:58 +0200 Subject: [PATCH 4/4] fix: enhance JWT validation by adding header checks and improving base64 decoding --- src/auth/validateJwtAndCheckExpiration.ts | 19 +++++++++--- .../validateJwtAndCheckExpiration.test.ts | 30 +++++++++++++++---- .../validateTokenAndCheckExpiration.test.ts | 8 ++--- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/auth/validateJwtAndCheckExpiration.ts b/src/auth/validateJwtAndCheckExpiration.ts index 7ab195e..77f1ccf 100644 --- a/src/auth/validateJwtAndCheckExpiration.ts +++ b/src/auth/validateJwtAndCheckExpiration.ts @@ -3,8 +3,12 @@ export interface DecodedJwtClaims { iat: number | null; } +function decodeBase64UrlSegment(seg: string): string { + return Buffer.from(seg.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString(); +} + /** - * Validates JWT token structure and parses the expiration and issued-at claims. + * Validates JWT token structure, header (presence of alg), and parses the expiration and issued-at claims. * Does not verify signature or issuer. * @returns The exp and iat claims (iat is null if absent), or null if invalid structure */ @@ -14,11 +18,18 @@ export default function validateJwtAndCheckExpiration(token: string): DecodedJwt } try { - const base64 = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'); - const payload = JSON.parse(Buffer.from(base64, 'base64').toString()); - if (typeof payload.exp !== 'number') { + const [headerSeg, payloadSeg] = token.split('.'); + + const header = JSON.parse(decodeBase64UrlSegment(headerSeg)); + if (typeof header.alg !== 'string') { return null; } + + const payload = JSON.parse(decodeBase64UrlSegment(payloadSeg)); + if (typeof payload.exp !== 'number' || payload.exp < 0) { + return null; + } + return { exp: payload.exp, iat: typeof payload.iat === 'number' ? payload.iat : null, diff --git a/test/auth/validateJwtAndCheckExpiration.test.ts b/test/auth/validateJwtAndCheckExpiration.test.ts index a75e303..8c5093d 100644 --- a/test/auth/validateJwtAndCheckExpiration.test.ts +++ b/test/auth/validateJwtAndCheckExpiration.test.ts @@ -12,26 +12,44 @@ describe('validateJwtAndCheckExpiration tests', () => { }); test('when the token payload is not valid base64 encoding, then null is returned', () => { - const invalidToken = 'header.!!!invalid_base64!!!.signature'; + const invalidToken = 'eyJhbGciOiJIUzI1NiJ9.!!!invalid_base64!!!.signature'; expect(validateJwtAndCheckExpiration(invalidToken)).to.be.equal(null); }); + test('when the token header is not valid JSON, then null is returned', () => { + const token = 'invalid-json.eyJzdWIiOiJ1c2VyMTIzIn0=.signature'; + expect(validateJwtAndCheckExpiration(token)).to.be.equal(null); + }); + + test('when the token header is missing the alg field, then null is returned', () => { + const header = Buffer.from(JSON.stringify({ typ: 'JWT' })).toString('base64'); + const payload = Buffer.from(JSON.stringify({ exp: Math.floor(Date.now() / 1000) + 3600 })).toString('base64'); + const token = `${header}.${payload}.signature`; + expect(validateJwtAndCheckExpiration(token)).to.be.equal(null); + }); + + test('when the token has a negative expiration value, then null is returned', () => { + const payload = Buffer.from(JSON.stringify({ exp: -1 })).toString('base64'); + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; + expect(validateJwtAndCheckExpiration(token)).to.be.equal(null); + }); + test('when the token does not contain an expiration claim, then null is returned', () => { const payload = Buffer.from(JSON.stringify({ sub: 'user123' })).toString('base64'); - const token = `header.${payload}.signature`; + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; expect(validateJwtAndCheckExpiration(token)).to.be.equal(null); }); test('when the token expiration value is not a number, then null is returned', () => { const payload = Buffer.from(JSON.stringify({ exp: 'not-a-number' })).toString('base64'); - const token = `header.${payload}.signature`; + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; expect(validateJwtAndCheckExpiration(token)).to.be.equal(null); }); test('when the token has a valid structure with expiration, then the exp and iat claims are returned', () => { const expiration = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now const payload = Buffer.from(JSON.stringify({ exp: expiration, sub: 'user123' })).toString('base64'); - const token = `header.${payload}.signature`; + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; expect(validateJwtAndCheckExpiration(token)).to.deep.equal({ exp: expiration, iat: null, @@ -47,7 +65,7 @@ describe('validateJwtAndCheckExpiration tests', () => { ); expect(payload).to.match(/-/); expect(payload).to.match(/_/); - const token = `header.${payload}.signature`; + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; expect(validateJwtAndCheckExpiration(token)).to.deep.equal({ exp: expiration, iat: issuedAt, @@ -58,7 +76,7 @@ describe('validateJwtAndCheckExpiration tests', () => { const issuedAt = Math.floor(Date.now() / 1000); const expiration = issuedAt + 3600; // 1 hour from now const payload = Buffer.from(JSON.stringify({ exp: expiration, iat: issuedAt, sub: 'user123' })).toString('base64'); - const token = `header.${payload}.signature`; + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; expect(validateJwtAndCheckExpiration(token)).to.deep.equal({ exp: expiration, iat: issuedAt, diff --git a/test/auth/validateTokenAndCheckExpiration.test.ts b/test/auth/validateTokenAndCheckExpiration.test.ts index 21714e1..53ab329 100644 --- a/test/auth/validateTokenAndCheckExpiration.test.ts +++ b/test/auth/validateTokenAndCheckExpiration.test.ts @@ -11,7 +11,7 @@ describe('validateTokenAndCheckExpiration tests', () => { test('when the token is structurally valid but has expired, then EXPIRED is returned', () => { const expiration = Math.floor(Date.now() / 1000) - 3600; // 1 hour ago const payload = Buffer.from(JSON.stringify({ exp: expiration })).toString('base64'); - const token = `header.${payload}.signature`; + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.EXPIRED); }); @@ -19,7 +19,7 @@ describe('validateTokenAndCheckExpiration tests', () => { test('when the token is valid and expires within six hours, then REFRESH_REQUIRED is returned', () => { const expiration = Math.floor(Date.now() / 1000) + 6 * 60 * 60; // 6 hours from now const payload = Buffer.from(JSON.stringify({ exp: expiration })).toString('base64'); - const token = `header.${payload}.signature`; + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.REFRESH_REQUIRED); }); @@ -28,7 +28,7 @@ describe('validateTokenAndCheckExpiration tests', () => { const issuedAt = Math.floor(Date.now() / 1000) - 16 * 60 * 60; // issued 16h ago const expiration = issuedAt + 32 * 60 * 60; // total lifetime 32h, 16h (50%) remaining const payload = Buffer.from(JSON.stringify({ exp: expiration, iat: issuedAt })).toString('base64'); - const token = `header.${payload}.signature`; + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.REFRESH_REQUIRED); }); @@ -36,7 +36,7 @@ describe('validateTokenAndCheckExpiration tests', () => { test('when the token is valid and expires in thirty days, then VALID is returned', () => { const expiration = Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; // 30 days from now const payload = Buffer.from(JSON.stringify({ exp: expiration })).toString('base64'); - const token = `header.${payload}.signature`; + const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`; expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.VALID); });