Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
18 changes: 15 additions & 3 deletions src/auth/validateJwtAndCheckExpiration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -14,10 +18,18 @@ export default function validateJwtAndCheckExpiration(token: string): DecodedJwt
}

try {
const payload = JSON.parse(atob(token.split('.')[1]));
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,
Expand Down
52 changes: 43 additions & 9 deletions test/auth/validateJwtAndCheckExpiration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,71 @@ 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 = btoa(JSON.stringify({ sub: 'user123' }));
const token = `header.${payload}.signature`;
const payload = Buffer.from(JSON.stringify({ sub: 'user123' })).toString('base64');
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 = btoa(JSON.stringify({ exp: 'not-a-number' }));
const token = `header.${payload}.signature`;
const payload = Buffer.from(JSON.stringify({ exp: 'not-a-number' })).toString('base64');
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 = btoa(JSON.stringify({ exp: expiration, sub: 'user123' }));
const token = `header.${payload}.signature`;
const payload = Buffer.from(JSON.stringify({ exp: expiration, sub: 'user123' })).toString('base64');
const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`;
expect(validateJwtAndCheckExpiration(token)).to.deep.equal({
exp: expiration,
iat: null,
});
});

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 = `eyJhbGciOiJIUzI1NiJ9.${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 token = `header.${payload}.signature`;
const payload = Buffer.from(JSON.stringify({ exp: expiration, iat: issuedAt, sub: 'user123' })).toString('base64');
const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`;
expect(validateJwtAndCheckExpiration(token)).to.deep.equal({
exp: expiration,
iat: issuedAt,
Expand Down
16 changes: 8 additions & 8 deletions test/auth/validateTokenAndCheckExpiration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,33 @@ 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 token = `header.${payload}.signature`;
const payload = Buffer.from(JSON.stringify({ exp: expiration })).toString('base64');
const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`;

expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.EXPIRED);
});

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 token = `header.${payload}.signature`;
const payload = Buffer.from(JSON.stringify({ exp: expiration })).toString('base64');
const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`;

expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.REFRESH_REQUIRED);
});

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 token = `header.${payload}.signature`;
const payload = Buffer.from(JSON.stringify({ exp: expiration, iat: issuedAt })).toString('base64');
const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`;

expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.REFRESH_REQUIRED);
});

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 token = `header.${payload}.signature`;
const payload = Buffer.from(JSON.stringify({ exp: expiration })).toString('base64');
const token = `eyJhbGciOiJIUzI1NiJ9.${payload}.signature`;

expect(validateTokenAndCheckExpiration(token)).to.be.equal(TokenStatus.VALID);
});
Expand Down
6 changes: 6 additions & 0 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"types": ["node"]
},
"include": ["./**/*.ts"]
}