mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 15:30:13 +01:00
203 lines
6.3 KiB
JavaScript
203 lines
6.3 KiB
JavaScript
/* eslint-env mocha */
|
|
import { expect } from 'chai';
|
|
import sinon from 'sinon';
|
|
import { Meteor } from 'meteor/meteor';
|
|
import { Accounts } from 'meteor/accounts-base';
|
|
|
|
describe('attachmentApi authentication', function() {
|
|
let findOneStub, hashStub;
|
|
|
|
beforeEach(function() {
|
|
hashStub = sinon.stub(Accounts, '_hashLoginToken');
|
|
findOneStub = sinon.stub(Meteor.users, 'findOne');
|
|
});
|
|
|
|
afterEach(function() {
|
|
if (hashStub) hashStub.restore();
|
|
if (findOneStub) findOneStub.restore();
|
|
});
|
|
|
|
// Mock request/response objects
|
|
function createMockReq(headers = {}) {
|
|
return {
|
|
headers,
|
|
on: sinon.stub(),
|
|
connection: { destroy: sinon.stub() },
|
|
};
|
|
}
|
|
|
|
function createMockRes() {
|
|
return {
|
|
writeHead: sinon.stub(),
|
|
end: sinon.stub(),
|
|
headersSent: false,
|
|
};
|
|
}
|
|
|
|
describe('authenticateApiRequest', function() {
|
|
it('denies request with missing X-User-Id header', function() {
|
|
const req = createMockReq({ 'x-auth-token': 'sometoken' });
|
|
const res = createMockRes();
|
|
|
|
// Simulate the handler behavior
|
|
let errorThrown = false;
|
|
try {
|
|
if (!req.headers['x-user-id'] || !req.headers['x-auth-token']) {
|
|
throw new Meteor.Error('unauthorized', 'Missing X-User-Id or X-Auth-Token headers');
|
|
}
|
|
} catch (error) {
|
|
errorThrown = true;
|
|
expect(error.error).to.equal('unauthorized');
|
|
}
|
|
|
|
expect(errorThrown).to.equal(true);
|
|
});
|
|
|
|
it('denies request with missing X-Auth-Token header', function() {
|
|
const req = createMockReq({ 'x-user-id': 'user123' });
|
|
|
|
let errorThrown = false;
|
|
try {
|
|
if (!req.headers['x-user-id'] || !req.headers['x-auth-token']) {
|
|
throw new Meteor.Error('unauthorized', 'Missing X-User-Id or X-Auth-Token headers');
|
|
}
|
|
} catch (error) {
|
|
errorThrown = true;
|
|
expect(error.error).to.equal('unauthorized');
|
|
}
|
|
|
|
expect(errorThrown).to.equal(true);
|
|
});
|
|
|
|
it('denies request with invalid token', function() {
|
|
const userId = 'user123';
|
|
const token = 'invalidtoken';
|
|
const req = createMockReq({ 'x-user-id': userId, 'x-auth-token': token });
|
|
|
|
hashStub.returns('hashedInvalidToken');
|
|
findOneStub.returns(null); // No user found
|
|
|
|
let errorThrown = false;
|
|
try {
|
|
const hashedToken = Accounts._hashLoginToken(token);
|
|
const user = Meteor.users.findOne({
|
|
_id: userId,
|
|
'services.resume.loginTokens.hashedToken': hashedToken,
|
|
});
|
|
if (!user) {
|
|
throw new Meteor.Error('unauthorized', 'Invalid credentials');
|
|
}
|
|
} catch (error) {
|
|
errorThrown = true;
|
|
expect(error.error).to.equal('unauthorized');
|
|
}
|
|
|
|
expect(errorThrown).to.equal(true);
|
|
expect(hashStub.calledOnce).to.equal(true);
|
|
expect(findOneStub.calledOnce).to.equal(true);
|
|
});
|
|
|
|
it('allows request with valid X-User-Id and X-Auth-Token', function() {
|
|
const userId = 'user123';
|
|
const token = 'validtoken';
|
|
const req = createMockReq({ 'x-user-id': userId, 'x-auth-token': token });
|
|
|
|
const hashedToken = 'hashedValidToken';
|
|
hashStub.returns(hashedToken);
|
|
findOneStub.returns({ _id: userId }); // User found
|
|
|
|
let authenticatedUserId = null;
|
|
try {
|
|
const hashed = Accounts._hashLoginToken(token);
|
|
const user = Meteor.users.findOne({
|
|
_id: userId,
|
|
'services.resume.loginTokens.hashedToken': hashed,
|
|
});
|
|
if (!user) {
|
|
throw new Meteor.Error('unauthorized', 'Invalid credentials');
|
|
}
|
|
authenticatedUserId = userId;
|
|
} catch (error) {
|
|
// Should not throw
|
|
}
|
|
|
|
expect(authenticatedUserId).to.equal(userId);
|
|
expect(hashStub.calledOnce).to.equal(true);
|
|
expect(hashStub.calledWith(token)).to.equal(true);
|
|
expect(findOneStub.calledOnce).to.equal(true);
|
|
const queryArg = findOneStub.getCall(0).args[0];
|
|
expect(queryArg._id).to.equal(userId);
|
|
expect(queryArg['services.resume.loginTokens.hashedToken']).to.equal(hashedToken);
|
|
});
|
|
|
|
it('prevents identity spoofing by validating hashed token', function() {
|
|
const victimId = 'victim-user-id';
|
|
const attackerToken = 'attacker-token';
|
|
const req = createMockReq({ 'x-user-id': victimId, 'x-auth-token': attackerToken });
|
|
|
|
hashStub.returns('hashedAttackerToken');
|
|
// Simulate victim exists but token doesn't match
|
|
findOneStub.returns(null);
|
|
|
|
let errorThrown = false;
|
|
try {
|
|
const hashed = Accounts._hashLoginToken(attackerToken);
|
|
const user = Meteor.users.findOne({
|
|
_id: victimId,
|
|
'services.resume.loginTokens.hashedToken': hashed,
|
|
});
|
|
if (!user) {
|
|
throw new Meteor.Error('unauthorized', 'Invalid credentials');
|
|
}
|
|
} catch (error) {
|
|
errorThrown = true;
|
|
expect(error.error).to.equal('unauthorized');
|
|
}
|
|
|
|
expect(errorThrown).to.equal(true);
|
|
});
|
|
});
|
|
|
|
describe('request handler DoS prevention', function() {
|
|
it('enforces timeout on hanging requests', function(done) {
|
|
this.timeout(5000);
|
|
|
|
const req = createMockReq({ 'x-user-id': 'user1', 'x-auth-token': 'token1' });
|
|
const res = createMockRes();
|
|
|
|
// Simulate timeout behavior
|
|
const timeout = setTimeout(() => {
|
|
if (!res.headersSent) {
|
|
res.headersSent = true;
|
|
res.writeHead(408, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ success: false, error: 'Request timeout' }));
|
|
}
|
|
}, 100); // Short timeout for test
|
|
|
|
// Wait for timeout
|
|
setTimeout(() => {
|
|
expect(res.headersSent).to.equal(true);
|
|
expect(res.writeHead.calledWith(408)).to.equal(true);
|
|
clearTimeout(timeout);
|
|
done();
|
|
}, 150);
|
|
});
|
|
|
|
it('limits request body size', function() {
|
|
const req = createMockReq({ 'x-user-id': 'user1', 'x-auth-token': 'token1' });
|
|
let body = '';
|
|
const limit = 50 * 1024 * 1024; // 50MB
|
|
|
|
// Simulate exceeding limit
|
|
body = 'a'.repeat(limit + 1);
|
|
expect(body.length).to.be.greaterThan(limit);
|
|
|
|
// Handler should destroy connection
|
|
if (body.length > limit) {
|
|
req.connection.destroy();
|
|
}
|
|
|
|
expect(req.connection.destroy.calledOnce).to.equal(true);
|
|
});
|
|
});
|
|
});
|