Migrate more tests to mocha.

This commit is contained in:
Clement Michaud 2019-02-13 23:04:57 +01:00
parent 7c2fd91271
commit c487ed0a37
23 changed files with 213 additions and 99 deletions

View File

@ -110,6 +110,7 @@
"should": "^13.2.1",
"sinon": "^5.0.7",
"tmp": "0.0.33",
"tree-kill": "^1.2.1",
"ts-node": "^6.0.1",
"tslint": "^5.2.0",
"typescript": "^2.9.2",

View File

@ -4,6 +4,7 @@ var program = require('commander');
var spawn = require('child_process').spawn;
var chokidar = require('chokidar');
var fs = require('fs');
var kill = require('tree-kill');
program
.option('-s, --suite <suite>', 'The suite to run Authelia for. This suite represents a configuration for Authelia and a set of tests for that configuration.')
@ -14,17 +15,18 @@ if (!program.suite) {
}
const ENVIRONMENT_FILENAME = '.suite';
const AUTHELIA_INTERRUPT_FILENAME = '.authelia-interrupt';
var tsWatcher = chokidar.watch(['server', 'shared/**/*.ts', 'node_modules'], {
var tsWatcher = chokidar.watch(['server', 'shared/**/*.ts', 'node_modules', AUTHELIA_INTERRUPT_FILENAME], {
persistent: true,
ignoreInitial: true,
});
// Properly cleanup server and client if ctrl-c is hit
process.on('SIGINT', function() {
killServer(() => {});
killClient(() => {});
killServer();
killClient();
fs.unlinkSync(ENVIRONMENT_FILENAME);
process.exit();
});
@ -38,7 +40,11 @@ function reloadServer() {
}
function startServer() {
serverProcess = spawn('./scripts/run-dev-server.sh', [`test/suites/${program.suite}/config.yml`], {detached: true});
if (fs.existsSync(AUTHELIA_INTERRUPT_FILENAME)) {
console.log('Authelia is interrupted. Consider removing ' + AUTHELIA_INTERRUPT_FILENAME + ' if it\'s not expected.');
return;
}
serverProcess = spawn('./scripts/run-dev-server.sh', [`test/suites/${program.suite}/config.yml`]);
serverProcess.stdout.pipe(process.stdout);
serverProcess.stderr.pipe(process.stderr);
}
@ -46,7 +52,6 @@ function startServer() {
let clientProcess;
function startClient() {
clientProcess = spawn('npm', ['run', 'start'], {
detached: true,
cwd: './client',
env: {
...process.env,
@ -61,14 +66,16 @@ function killServer(onExit) {
if (serverProcess) {
serverProcess.on('exit', () => {
serverProcess = undefined;
onExit();
if (onExit) onExit();
});
try {
process.kill(-serverProcess.pid);
kill(serverProcess.pid, 'SIGKILL');
} catch (e) {
console.error(e);
onExit();
if (onExit) onExit();
}
} else {
if (onExit) onExit();
}
}
@ -76,14 +83,16 @@ function killClient(onExit) {
if (clientProcess) {
clientProcess.on('exit', () => {
clientProcess = undefined;
onExit();
if (onExit) onExit();
});
try {
process.kill(-clientProcess.pid);
kill(clientProcess.pid, 'SIGKILL');
} catch (e) {
console.error(e);
onExit();
if (onExit) onExit();
}
} else {
if (onExit) onExit();
}
}
@ -97,6 +106,16 @@ function reload(path) {
console.log('Schema needs to be regenerated.');
generateConfigurationSchema();
}
else if (path === AUTHELIA_INTERRUPT_FILENAME) {
if (fs.existsSync(path)) {
console.log('Authelia is being interrupted.');
killServer();
} else {
console.log('Authelia is restarting.');
startServer();
}
return;
}
reloadServer();
}
@ -128,7 +147,7 @@ async function main() {
console.log('Start watching...');
tsWatcher.on('add', reload);
tsWatcher.on('remove', reload);
tsWatcher.on('unlink', reload);
tsWatcher.on('change', reload);
startServer();

View File

@ -0,0 +1,11 @@
// Error thrown when the authentication failed when checking
// user/password.
class AuthenticationError extends Error {
constructor(msg: string) {
super(msg);
}
}
export default AuthenticationError

View File

@ -8,6 +8,7 @@ import { GroupsAndEmails } from "../GroupsAndEmails";
import { IUsersDatabase } from "../IUsersDatabase";
import { HashGenerator } from "../../../utils/HashGenerator";
import { ReadWriteQueue } from "./ReadWriteQueue";
import AuthenticationError from "../../AuthenticationError";
const loadAsync = Bluebird.promisify(Yaml.load);
@ -80,7 +81,7 @@ export class FileUsersDatabase implements IUsersDatabase {
return HashGenerator.ssha512(password, rounds, salt)
.then((hash: string) => {
if (hash !== storedHash) {
return Bluebird.reject(new Error("Wrong username/password."));
return Bluebird.reject(new AuthenticationError("Wrong username/password."));
}
return Bluebird.resolve();
});

View File

@ -5,6 +5,7 @@ import { LdapConfiguration } from "../../../configuration/schema/LdapConfigurati
import { ISession } from "./ISession";
import { GroupsAndEmails } from "../GroupsAndEmails";
import Exceptions = require("../../../Exceptions");
import AuthenticationError from "../../AuthenticationError";
type SessionCallback<T> = (session: ISession) => Bluebird<T>;
@ -58,7 +59,7 @@ export class LdapUsersDatabase implements IUsersDatabase {
.then(() => getInfo(session));
})
.catch((err) =>
Bluebird.reject(new Exceptions.LdapError(err.message)));
Bluebird.reject(new AuthenticationError(err.message)));
}
getEmails(username: string): Bluebird<string[]> {

View File

@ -15,6 +15,7 @@ import { BelongToDomain } from "../../../../../shared/BelongToDomain";
import { URLDecomposer } from "../..//utils/URLDecomposer";
import { Object } from "../../../lib/authorization/Object";
import { Subject } from "../../../lib/authorization/Subject";
import AuthenticationError from "../../../lib/authentication/AuthenticationError";
export default function (vars: ServerVariables) {
return function (req: express.Request, res: express.Response)
@ -95,7 +96,7 @@ export default function (vars: ServerVariables) {
res.send();
return BluebirdPromise.resolve();
})
.catch(Exceptions.LdapBindError, function (err: Error) {
.catch(AuthenticationError, function (err: Error) {
vars.regulator.mark(username, false);
return ErrorReplies.replyWithError200(req, res, vars.logger, UserMessages.AUTHENTICATION_FAILED)(err);
})

View File

@ -1,39 +0,0 @@
@needs-regulation-config
Feature: Authelia regulates authentication to avoid brute force
@need-registered-user-blackhat
Scenario: Attacker tries too many authentication in a short period of time and get banned
Given I visit "https://login.example.com:8080/"
And I set field "username" to "blackhat"
And I set field "password" to "bad-password"
And I click on "Sign in"
And I get a notification of type "error" with message "Authentication failed. Please check your credentials."
And I set field "password" to "bad-password"
And I click on "Sign in"
And I get a notification of type "error" with message "Authentication failed. Please check your credentials."
And I set field "password" to "bad-password"
And I click on "Sign in"
And I get a notification of type "error" with message "Authentication failed. Please check your credentials."
When I set field "password" to "password"
And I click on "Sign in"
Then I get a notification of type "error" with message "Authentication failed. Please check your credentials."
@need-registered-user-blackhat
Scenario: User is unbanned after a configured amount of time
Given I visit "https://login.example.com:8080/?rd=https://public.example.com:8080/secret.html"
And I set field "username" to "blackhat"
And I set field "password" to "bad-password"
And I click on "Sign in"
And I get a notification of type "error" with message "Authentication failed. Please check your credentials."
And I set field "password" to "bad-password"
And I click on "Sign in"
And I get a notification of type "error" with message "Authentication failed. Please check your credentials."
And I set field "password" to "bad-password"
And I click on "Sign in"
And I get a notification of type "error" with message "Authentication failed. Please check your credentials."
When I wait 6 seconds
And I set field "password" to "password"
And I click on "Sign in"
And I use "REGISTERED" as TOTP token handle
And I click on "Sign in"
Then I'm redirected to "https://public.example.com:8080/secret.html"

View File

@ -1,15 +0,0 @@
Feature: Authelia keeps user sessions despite the application restart
@need-authenticated-user-john
Scenario: Session is still valid after Authelia restarts
When the application restarts
Then I have access to "https://admin.example.com:8080/secret.html"
@need-registered-user-john
Scenario: Secrets are stored even when Authelia restarts
When the application restarts
And I visit "https://admin.example.com:8080/secret.html" and get redirected "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html"
And I login with user "john" and password "password"
And I use "REGISTERED" as TOTP token handle
And I click on "Sign in"
Then I'm redirected to "https://admin.example.com:8080/secret.html"

View File

@ -1,16 +0,0 @@
Feature: User can access certain subdomains with single factor
Scenario: User is redirected to service after first factor if allowed
When I visit "https://login.example.com:8080/?rd=https://single_factor.example.com:8080/secret.html"
And I login with user "john" and password "password"
Then I'm redirected to "https://single_factor.example.com:8080/secret.html"
Scenario: Redirection after first factor fails if single_factor not allowed. It redirects user to first factor.
When I visit "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html"
And I login with user "john" and password "password"
Then I'm redirected to "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html"
Scenario: User can login using basic authentication
When I request "https://single_factor.example.com:8080/secret.html" with username "john" and password "password" using basic authentication
Then I receive the secret page

View File

@ -1,7 +1,7 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
import Assert = require("assert");
export default async function(driver: WebDriver, type: string, message: string) {
export default async function(driver: WebDriver, message: string) {
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("notification")), 5000)
const notificationEl = driver.findElement(SeleniumWebdriver.By.className("notification"));
const txt = await notificationEl.getText();

View File

@ -0,0 +1,9 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
import Assert = require("assert");
export default async function(driver: WebDriver, message: string) {
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("notification")), 5000)
const notificationEl = driver.findElement(SeleniumWebdriver.By.className("notification"));
const txt = await notificationEl.getText();
Assert.equal(message, txt);
}

View File

@ -5,8 +5,7 @@ import { StatusCodeError } from 'request-promise/errors';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
// Sent a GET request to the url and expect a 401
export async function GET_Expect401(url: string) {
export async function GET_ExpectError(url: string, statusCode: number) {
try {
await Request.get(url, {
json: true,
@ -15,13 +14,22 @@ export async function GET_Expect401(url: string) {
throw new Error('No response');
} catch (e) {
if (e instanceof StatusCodeError) {
Assert.equal(e.statusCode, 401);
Assert.equal(e.statusCode, statusCode);
return;
}
}
return;
}
// Sent a GET request to the url and expect a 401
export async function GET_Expect401(url: string) {
return await GET_ExpectError(url, 401);
}
export async function GET_Expect502(url: string) {
return await GET_ExpectError(url, 502);
}
export async function POST_Expect401(url: string, body?: any) {
try {
await Request.post(url, {

View File

@ -213,10 +213,10 @@ regulation:
# The time range during which the user can attempt login before being banned.
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
find_time: 120
find_time: 15
# The length of time before a banned user can login again.
ban_time: 300
ban_time: 5
# Configuration of the storage backend used to store data and secrets.
#

View File

@ -5,6 +5,8 @@ import AccessControl from "./scenarii/AccessControl";
import CustomHeadersForwarded from "./scenarii/CustomHeadersForwarded";
import SingleFactorAuthentication from "./scenarii/SingleFactorAuthentication";
import BasicAuthentication from "./scenarii/BasicAuthentication";
import AutheliaRestart from "./scenarii/AutheliaRestart";
import AuthenticationRegulation from "./scenarii/AuthenticationRegulation";
AutheliaSuite('Complete configuration', __dirname + '/config.yml', function() {
this.timeout(10000);
@ -16,4 +18,6 @@ AutheliaSuite('Complete configuration', __dirname + '/config.yml', function() {
describe('Enforce internal redirections only', EnforceInternalRedirectionsOnly);
describe('Single factor authentication', SingleFactorAuthentication);
describe('Basic authentication', BasicAuthentication);
describe('Authelia restart', AutheliaRestart);
describe('Authentication regulation', AuthenticationRegulation);
});

View File

@ -0,0 +1,72 @@
import Logout from "../../../helpers/Logout";
import ChildProcess from 'child_process';
import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver";
import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved";
import RegisterAndLoginTwoFactor from "../../../helpers/behaviors/RegisterAndLoginTwoFactor";
import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs";
import { GET_Expect502 } from "../../../helpers/utils/Requests";
import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp";
import FullLogin from "../../../helpers/FullLogin";
export default function() {
describe('Session is still valid after Authelia restarts', function() {
before(async function() {
// Be sure to start fresh
ChildProcess.execSync('rm -f .authelia-interrupt');
this.driver = await StartDriver();
await RegisterAndLoginTwoFactor(this.driver, 'john', true, 'https://admin.example.com:8080/secret.html');
await VisitPageAndWaitUrlIs(this.driver, 'https://home.example.com:8080/');
});
after(async function() {
await Logout(this.driver);
await StopDriver(this.driver);
// Be sure to cleanup
ChildProcess.execSync('rm -f .authelia-interrupt');
});
it("should still access the secret after Authelia restarted", async function() {
ChildProcess.execSync('touch .authelia-interrupt');
await GET_Expect502('https://login.example.com:8080/api/state');
await this.driver.sleep(1000);
ChildProcess.execSync('rm .authelia-interrupt');
await this.driver.sleep(1000);
await VisitPageAndWaitUrlIs(this.driver, 'https://admin.example.com:8080/secret.html');
await VerifySecretObserved(this.driver);
});
});
describe('Secrets are persisted even if Authelia restarts', function() {
before(async function() {
// Be sure to start fresh
ChildProcess.execSync('rm -f .authelia-interrupt');
this.driver = await StartDriver();
this.secret = await LoginAndRegisterTotp(this.driver, 'john', true);
await Logout(this.driver);
});
after(async function() {
await Logout(this.driver);
await StopDriver(this.driver);
// Be sure to cleanup
ChildProcess.execSync('rm -f .authelia-interrupt');
});
it("should still access the secret after Authelia restarted", async function() {
ChildProcess.execSync('touch .authelia-interrupt');
await GET_Expect502('https://login.example.com:8080/api/state');
await this.driver.sleep(1000);
ChildProcess.execSync('rm .authelia-interrupt');
await this.driver.sleep(1000);
// The user can re-authenticate with the secret.
await FullLogin(this.driver, 'john', this.secret, 'https://admin.example.com:8080/secret.html')
});
});
}

View File

@ -0,0 +1,53 @@
import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver";
import LoginAs from "../../../helpers/LoginAs";
import VerifyNotificationDisplayed from "../../../helpers/assertions/VerifyNotificationDisplayed";
import VerifyIsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage";
/*
Given I visit "https://login.example.com:8080/"
And I set field "username" to "blackhat"
And I set field "password" to "bad-password"
And I click on "Sign in"
And I get a notification of type "error" with message "Authentication failed. Please check your credentials."
And I set field "password" to "bad-password"
And I click on "Sign in"
And I get a notification of type "error" with message "Authentication failed. Please check your credentials."
And I set field "password" to "bad-password"
And I click on "Sign in"
And I get a notification of type "error" with message "Authentication failed. Please check your credentials."
When I set field "password" to "password"
And I click on "Sign in"
Then I get a notification of type "error" with message "Authentication failed. Please check your credentials."
*/
export default function() {
describe('Authelia regulates authentications when a hacker is brute forcing', function() {
this.timeout(15000);
before(async function() {
this.driver = await StartDriver();
});
after(async function() {
await StopDriver(this.driver);
});
it("should return an error message when providing correct credentials the 4th time.", async function() {
await LoginAs(this.driver, "blackhat", "bad-password");
await VerifyNotificationDisplayed(this.driver, "Authentication failed. Please check your credentials.");
await LoginAs(this.driver, "blackhat", "bad-password");
await VerifyNotificationDisplayed(this.driver, "Authentication failed. Please check your credentials.");
await LoginAs(this.driver, "blackhat", "bad-password");
await VerifyNotificationDisplayed(this.driver, "Authentication failed. Please check your credentials.");
// when providing good credentials, the hacker is regulated and see same message as previously.
await LoginAs(this.driver, "blackhat", "password");
await VerifyNotificationDisplayed(this.driver, "Authentication failed. Please check your credentials.");
// Wait the regulation ban time before retrying with correct credentials.
// It should authenticate normally.
await this.driver.sleep(6000);
await LoginAs(this.driver, "blackhat", "password");
await VerifyIsSecondFactorStage(this.driver);
});
});
}

View File

@ -6,6 +6,9 @@ import VisitPage from "../../../helpers/VisitPage";
import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs";
import VerifyIsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage";
/*
* Those tests are related to single factor protected resources.
*/
export default function() {
beforeEach(async function() {
this.driver = await StartDriver();

View File

@ -73,10 +73,10 @@ regulation:
max_retries: 3
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
find_time: 120
find_time: 10
# The length of time before a banned user can login again.
ban_time: 300
ban_time: 5
# Default redirection URL
#

View File

@ -17,7 +17,7 @@ export default function() {
it('should get a notification message', async function () {
this.timeout(10000);
await SeeNotification(this.driver, "error", AUTHENTICATION_FAILED);
await SeeNotification(this.driver, AUTHENTICATION_FAILED);
});
});
}

View File

@ -4,6 +4,8 @@ import ValidateTotp from "../../../helpers/ValidateTotp";
import WaitRedirected from "../../../helpers/WaitRedirected";
import { WebDriver } from "selenium-webdriver";
import VisitPageAndWaitUrlIs from "../../../helpers/behaviors/VisitPageAndWaitUrlIs";
import VisitPage from "../../../helpers/VisitPage";
import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs";
export default function(this: Mocha.ISuiteCallbackContext) {
this.timeout(20000);
@ -52,8 +54,8 @@ export default function(this: Mocha.ISuiteCallbackContext) {
await WaitRedirected(driver, "https://admin.example.com:8080/secret.html");
await VisitPageAndWaitUrlIs(driver, "https://home.example.com:8080/");
await driver.sleep(6000);
await driver.get("https://admin.example.com:8080/secret.html");
await WaitRedirected(driver, "https://admin.example.com:8080/secret.html");
await VisitPage(driver, "https://admin.example.com:8080/secret.html");
await VerifyUrlIs(driver, "https://admin.example.com:8080/secret.html");
});
});
}

View File

@ -29,7 +29,6 @@ export default function() {
});
it("should have user and issuer in otp url", async function() {
// this.timeout(100000);
const el = await (this.driver as WebDriver).wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.className('otpauth-secret')), 5000);

View File

@ -57,6 +57,6 @@ export default function() {
await FillField(this.driver, "password1", "newpass");
await FillField(this.driver, "password2", "badpass");
await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button'));
await SeeNotification(this.driver, "error", "The passwords are different.");
await SeeNotification(this.driver, "The passwords are different.");
});
}

View File

@ -45,7 +45,7 @@ export default function() {
});
it("get a notification message", async function() {
await SeeNotification(this.driver, "error", AUTHENTICATION_TOTP_FAILED);
await SeeNotification(this.driver, AUTHENTICATION_TOTP_FAILED);
});
});
}