mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(core): Prevent session hijacking (#9057)
This commit is contained in:
committed by
GitHub
parent
5793e5644a
commit
28261047c3
@@ -1,6 +1,6 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { type NextFunction, type Response } from 'express';
|
||||
import type { NextFunction, Response } from 'express';
|
||||
|
||||
import { AuthService } from '@/auth/auth.service';
|
||||
import config from '@/config';
|
||||
@@ -14,6 +14,7 @@ import type { AuthenticatedRequest } from '@/requests';
|
||||
describe('AuthService', () => {
|
||||
config.set('userManagement.jwtSecret', 'random-secret');
|
||||
|
||||
const browserId = 'test-browser-id';
|
||||
const userData = {
|
||||
id: '123',
|
||||
email: 'test@example.com',
|
||||
@@ -21,17 +22,18 @@ describe('AuthService', () => {
|
||||
disabled: false,
|
||||
mfaEnabled: false,
|
||||
};
|
||||
const validToken =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsImhhc2giOiJtSkFZeDRXYjdrIiwiaWF0IjoxNzA2NzUwNjI1LCJleHAiOjE3MDczNTU0MjV9.JwY3doH0YrxHdX4nTOlTN4-QMaXsAu5OFOaFcIHSHBI';
|
||||
|
||||
const user = mock<User>(userData);
|
||||
const jwtService = new JwtService(mock());
|
||||
const urlService = mock<UrlService>();
|
||||
const userRepository = mock<UserRepository>();
|
||||
const authService = new AuthService(mock(), mock(), jwtService, urlService, userRepository);
|
||||
|
||||
jest.useFakeTimers();
|
||||
const now = new Date('2024-02-01T01:23:45.678Z');
|
||||
jest.useFakeTimers({ now });
|
||||
|
||||
const validToken =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsImhhc2giOiJtSkFZeDRXYjdrIiwiYnJvd3NlcklkIjoiOFpDVXE1YU1uSFhnMFZvcURLcm9hMHNaZ0NwdWlPQ1AzLzB2UmZKUXU0MD0iLCJpYXQiOjE3MDY3NTA2MjUsImV4cCI6MTcwNzM1NTQyNX0.YE-ZGGIQRNQ4DzUe9rjXvOOFFN9ufU34WibsCxAsc4o'; // Generated using `authService.issueJWT(user, browserId)`
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.setSystemTime(now);
|
||||
@@ -54,7 +56,11 @@ describe('AuthService', () => {
|
||||
});
|
||||
|
||||
describe('authMiddleware', () => {
|
||||
const req = mock<AuthenticatedRequest>({ cookies: {}, user: undefined });
|
||||
const req = mock<AuthenticatedRequest>({
|
||||
cookies: {},
|
||||
user: undefined,
|
||||
browserId,
|
||||
});
|
||||
const res = mock<Response>();
|
||||
const next = jest.fn() as NextFunction;
|
||||
|
||||
@@ -99,7 +105,7 @@ describe('AuthService', () => {
|
||||
describe('when not setting userManagement.jwtSessionDuration', () => {
|
||||
it('should default to expire in 7 days', () => {
|
||||
const defaultInSeconds = 7 * Time.days.toSeconds;
|
||||
const token = authService.issueJWT(user);
|
||||
const token = authService.issueJWT(user, browserId);
|
||||
|
||||
expect(authService.jwtExpiration).toBe(defaultInSeconds);
|
||||
const decodedToken = jwtService.verify(token);
|
||||
@@ -117,7 +123,7 @@ describe('AuthService', () => {
|
||||
|
||||
it('should apply it to tokens', () => {
|
||||
config.set('userManagement.jwtSessionDurationHours', testDurationHours);
|
||||
const token = authService.issueJWT(user);
|
||||
const token = authService.issueJWT(user, browserId);
|
||||
|
||||
const decodedToken = jwtService.verify(token);
|
||||
if (decodedToken.exp === undefined || decodedToken.iat === undefined) {
|
||||
@@ -129,24 +135,40 @@ describe('AuthService', () => {
|
||||
});
|
||||
|
||||
describe('resolveJwt', () => {
|
||||
const req = mock<AuthenticatedRequest>({
|
||||
cookies: {},
|
||||
user: undefined,
|
||||
browserId,
|
||||
});
|
||||
const res = mock<Response>();
|
||||
|
||||
it('should throw on invalid tokens', async () => {
|
||||
await expect(authService.resolveJwt('random-string', res)).rejects.toThrow('jwt malformed');
|
||||
await expect(authService.resolveJwt('random-string', req, res)).rejects.toThrow(
|
||||
'jwt malformed',
|
||||
);
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw on expired tokens', async () => {
|
||||
jest.advanceTimersByTime(365 * Time.days.toMilliseconds);
|
||||
|
||||
await expect(authService.resolveJwt(validToken, res)).rejects.toThrow('jwt expired');
|
||||
await expect(authService.resolveJwt(validToken, req, res)).rejects.toThrow('jwt expired');
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw on tampered tokens', async () => {
|
||||
const [header, payload, signature] = validToken.split('.');
|
||||
const tamperedToken = [header, payload, signature + '123'].join('.');
|
||||
await expect(authService.resolveJwt(tamperedToken, res)).rejects.toThrow('invalid signature');
|
||||
await expect(authService.resolveJwt(tamperedToken, req, res)).rejects.toThrow(
|
||||
'invalid signature',
|
||||
);
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw on hijacked tokens', async () => {
|
||||
userRepository.findOne.mockResolvedValue(user);
|
||||
const req = mock<AuthenticatedRequest>({ browserId: 'another-browser' });
|
||||
await expect(authService.resolveJwt(validToken, req, res)).rejects.toThrow('Unauthorized');
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -163,17 +185,17 @@ describe('AuthService', () => {
|
||||
],
|
||||
])('should throw if %s', async (_, data) => {
|
||||
userRepository.findOne.mockResolvedValueOnce(data && mock<User>(data));
|
||||
await expect(authService.resolveJwt(validToken, res)).rejects.toThrow('Unauthorized');
|
||||
await expect(authService.resolveJwt(validToken, req, res)).rejects.toThrow('Unauthorized');
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should refresh the cookie before it expires', async () => {
|
||||
userRepository.findOne.mockResolvedValue(user);
|
||||
expect(await authService.resolveJwt(validToken, res)).toEqual(user);
|
||||
expect(await authService.resolveJwt(validToken, req, res)).toEqual(user);
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(6 * Time.days.toMilliseconds); // 6 Days
|
||||
expect(await authService.resolveJwt(validToken, res)).toEqual(user);
|
||||
expect(await authService.resolveJwt(validToken, req, res)).toEqual(user);
|
||||
expect(res.cookie).toHaveBeenCalledWith('n8n-auth', expect.any(String), {
|
||||
httpOnly: true,
|
||||
maxAge: 604800000,
|
||||
@@ -184,15 +206,15 @@ describe('AuthService', () => {
|
||||
|
||||
it('should refresh the cookie only if less than 1/4th of time is left', async () => {
|
||||
userRepository.findOne.mockResolvedValue(user);
|
||||
expect(await authService.resolveJwt(validToken, res)).toEqual(user);
|
||||
expect(await authService.resolveJwt(validToken, req, res)).toEqual(user);
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(5 * Time.days.toMilliseconds);
|
||||
expect(await authService.resolveJwt(validToken, res)).toEqual(user);
|
||||
expect(await authService.resolveJwt(validToken, req, res)).toEqual(user);
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(1 * Time.days.toMilliseconds);
|
||||
expect(await authService.resolveJwt(validToken, res)).toEqual(user);
|
||||
expect(await authService.resolveJwt(validToken, req, res)).toEqual(user);
|
||||
expect(res.cookie).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -200,11 +222,11 @@ describe('AuthService', () => {
|
||||
config.set('userManagement.jwtRefreshTimeoutHours', -1);
|
||||
|
||||
userRepository.findOne.mockResolvedValue(user);
|
||||
expect(await authService.resolveJwt(validToken, res)).toEqual(user);
|
||||
expect(await authService.resolveJwt(validToken, req, res)).toEqual(user);
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
|
||||
jest.advanceTimersByTime(6 * Time.days.toMilliseconds); // 6 Days
|
||||
expect(await authService.resolveJwt(validToken, res)).toEqual(user);
|
||||
expect(await authService.resolveJwt(validToken, req, res)).toEqual(user);
|
||||
expect(res.cookie).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user