Files
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

138 lines
5.0 KiB
TypeScript

import jwt from 'jsonwebtoken';
import { describe, expect, it, vi, afterEach } from 'vitest';
vi.hoisted(() => {
process.env.JWT_SECRET = 'test-secret-long-enough-for-signing-32chars';
});
import { AppError } from '../../src/utils/errors';
import { TokenManager } from '../../src/infra/security/token.manager';
describe('TokenManager refresh CSRF tokens', () => {
const userId = 'user-csrf-1';
const tokenManager = TokenManager.getInstance();
afterEach(() => {
vi.useRealTimers();
});
it('keeps the CSRF token stable when a refresh token is reissued with the same nonce', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-05-19T12:00:00.000Z'));
const initialRefreshToken = tokenManager.generateRefreshToken(userId, 'user');
const initialPayload = tokenManager.verifyRefreshToken(initialRefreshToken);
const initialCsrfToken = tokenManager.generateCsrfToken(initialPayload);
vi.setSystemTime(new Date('2026-05-19T12:02:00.000Z'));
const rotatedRefreshToken = tokenManager.generateRefreshToken(
initialPayload.sub,
initialPayload.sessionType,
initialPayload.csrfNonce
);
const rotatedPayload = tokenManager.verifyRefreshToken(rotatedRefreshToken);
const rotatedCsrfToken = tokenManager.generateCsrfToken(rotatedPayload);
expect(rotatedRefreshToken).not.toBe(initialRefreshToken);
expect(rotatedPayload.csrfNonce).toBe(initialPayload.csrfNonce);
expect(rotatedPayload.sessionType).toBe('user');
expect(rotatedCsrfToken).toBe(initialCsrfToken);
expect(tokenManager.verifyCsrfToken(initialCsrfToken, rotatedPayload)).toBe(true);
});
it('rejects CSRF tokens from another refresh-token family', () => {
const firstRefreshToken = tokenManager.generateRefreshToken(userId, 'user');
const secondRefreshToken = tokenManager.generateRefreshToken(userId, 'user');
const firstCsrfToken = tokenManager.generateCsrfToken(
tokenManager.verifyRefreshToken(firstRefreshToken)
);
const secondPayload = tokenManager.verifyRefreshToken(secondRefreshToken);
expect(tokenManager.verifyCsrfToken(firstCsrfToken, secondPayload)).toBe(false);
});
it('separates user and admin refresh token session types', () => {
const userRefreshToken = tokenManager.generateRefreshToken(userId, 'user');
const adminRefreshToken = tokenManager.generateRefreshToken(userId, 'admin');
expect(tokenManager.verifyRefreshToken(userRefreshToken).sessionType).toBe('user');
expect(tokenManager.verifyRefreshToken(adminRefreshToken).sessionType).toBe('admin');
});
it('generates a matching refresh token and CSRF token together', () => {
const { refreshToken, csrfToken } = tokenManager.generateRefreshTokenWithCsrf(userId, 'user');
const payload = tokenManager.verifyRefreshToken(refreshToken);
expect(payload.sub).toBe(userId);
expect(payload.sessionType).toBe('user');
expect(tokenManager.verifyCsrfToken(csrfToken, payload)).toBe(true);
});
it('includes refresh session type in CSRF derivation', () => {
const csrfNonce = 'shared-test-nonce';
const userRefreshToken = tokenManager.generateRefreshToken(userId, 'user', csrfNonce);
const adminRefreshToken = tokenManager.generateRefreshToken(userId, 'admin', csrfNonce);
expect(
tokenManager.generateCsrfToken(tokenManager.verifyRefreshToken(userRefreshToken))
).not.toBe(tokenManager.generateCsrfToken(tokenManager.verifyRefreshToken(adminRefreshToken)));
});
it('rejects legacy refresh tokens that do not carry csrf/session claims', () => {
const legacyRefreshToken = jwt.sign(
{
sub: userId,
type: 'refresh',
iss: 'insforge',
},
process.env.JWT_SECRET ?? '',
{
algorithm: 'HS256',
expiresIn: '7d',
}
);
expect(() => tokenManager.verifyRefreshToken(legacyRefreshToken)).toThrow(AppError);
});
it('rejects refresh tokens that do not carry a session type', () => {
const legacyRefreshToken = jwt.sign(
{
sub: userId,
type: 'refresh',
iss: 'insforge',
csrfNonce: 'nonce',
},
process.env.JWT_SECRET ?? '',
{
algorithm: 'HS256',
expiresIn: '7d',
}
);
expect(() => tokenManager.verifyRefreshToken(legacyRefreshToken)).toThrow(AppError);
});
it('generates PostgREST anon tokens without a fake anonymous subject', () => {
const anonToken = tokenManager.generatePostgrestAnonToken();
const payload = jwt.verify(anonToken, process.env.JWT_SECRET ?? '') as Record<string, unknown>;
expect(payload.role).toBe('anon');
expect(payload.sub).toBeUndefined();
expect(payload.email).toBeUndefined();
});
it('generates PostgREST admin tokens without a fake admin subject', () => {
const postgrestToken = tokenManager.generatePostgrestAdminToken();
const payload = jwt.verify(postgrestToken, process.env.JWT_SECRET ?? '') as Record<
string,
unknown
>;
expect(payload.role).toBe('project_admin');
expect(payload.sub).toBeUndefined();
expect(payload.email).toBeUndefined();
});
});