Skip to content
This repository was archived by the owner on May 19, 2026. It is now read-only.
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
81 changes: 80 additions & 1 deletion apps/server/src/core/auth/services/token.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
jest.mock('../../../common/helpers', () => ({
isUserDisabled: () => false,
}));

import { Test, TestingModule } from '@nestjs/testing';
import { JwtModule } from '@nestjs/jwt';
import * as jwt from 'jsonwebtoken';
import { TokenService } from './token.service';
import { EnvironmentService } from '../../../integrations/environment/environment.service';
import { createMockUser } from '../../../test-utils/test-helpers';

describe('TokenService', () => {
let service: TokenService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [TokenService],
imports: [
// Mirror production: a module-level expiresIn default exists, intended
// for short-lived access tokens. generateApiToken must NOT inherit it.
JwtModule.register({
secret: 'test-secret',
signOptions: { expiresIn: '90d', issuer: 'Docmost' },
}),
],
providers: [
TokenService,
{
provide: EnvironmentService,
useValue: { getAppSecret: () => 'test-secret' },
},
],
}).compile();

service = module.get<TokenService>(TokenService);
Expand All @@ -15,4 +37,61 @@ describe('TokenService', () => {
it('should be defined', () => {
expect(service).toBeDefined();
});

describe('generateApiToken', () => {
const user = createMockUser();

it('uses a long-lived exp (~100y) when no expiresIn is supplied, instead of inheriting the 90d module default', async () => {
const before = Math.floor(Date.now() / 1000);

const token = await service.generateApiToken({
apiKeyId: 'key-id-1',
user,
workspaceId: user.workspaceId,
});

const decoded = jwt.decode(token) as {
exp?: number;
iat?: number;
type?: string;
apiKeyId?: string;
sub?: string;
workspaceId?: string;
} | null;
expect(decoded).not.toBeNull();
expect(decoded!.type).toBe('api_key');
expect(decoded!.apiKeyId).toBe('key-id-1');
expect(decoded!.sub).toBe(user.id);
expect(decoded!.workspaceId).toBe(user.workspaceId);

// Must NOT be the leaky 90d module default (the bug). Anything past one
// year out is fine; we use 100y in production.
const ninetyDaysSeconds = 90 * 24 * 60 * 60;
const oneYearSeconds = 365 * 24 * 60 * 60;
expect(decoded!.iat).toBeDefined();
expect(decoded!.exp).toBeDefined();
expect(decoded!.iat!).toBeGreaterThanOrEqual(before);
const ttl = decoded!.exp! - decoded!.iat!;
expect(ttl).toBeGreaterThan(oneYearSeconds);
expect(ttl).not.toBe(ninetyDaysSeconds);
});

it('respects an explicit numeric expiresIn (seconds) when supplied', async () => {
const before = Math.floor(Date.now() / 1000);

const token = await service.generateApiToken({
apiKeyId: 'key-id-2',
user,
workspaceId: user.workspaceId,
expiresIn: 3600,
});

const decoded = jwt.decode(token) as { exp?: number; iat?: number } | null;
expect(decoded).not.toBeNull();
expect(decoded!.exp).toBeDefined();
expect(decoded!.iat).toBeDefined();
expect(decoded!.exp! - decoded!.iat!).toBe(3600);
expect(decoded!.iat!).toBeGreaterThanOrEqual(before);
});
});
});
4 changes: 3 additions & 1 deletion apps/server/src/core/auth/services/token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ export class TokenService {
type: JwtType.API_KEY,
};

return this.jwtService.sign(payload, expiresIn ? { expiresIn } : {});
// due to the default value of 90 days, if no expiry is provided
// 100y are set as a maximum length
return this.jwtService.sign(payload, { expiresIn: expiresIn ?? '100y' });
}

async verifyJwt(token: string, tokenType: string) {
Expand Down
Loading