Migrate some tests to mocha.

pull/330/head
Clement Michaud 2019-02-09 23:20:37 +01:00
parent c5af4498ab
commit efceb66ffa
35 changed files with 398 additions and 228 deletions

View File

@ -20,6 +20,10 @@
word-wrap: break-word; word-wrap: break-word;
} }
.otpauthContainer {
display: none;
}
.text { .text {
text-align: center; text-align: center;
} }

View File

@ -58,6 +58,7 @@ class OneTimePasswordRegistrationView extends Component<Props> {
<div className={classnames(styles.qrcodeContainer, 'qrcode')}> <div className={classnames(styles.qrcodeContainer, 'qrcode')}>
<QRCode value={secret.otpauth_url} size={180} level="Q"></QRCode> <QRCode value={secret.otpauth_url} size={180} level="Q"></QRCode>
</div> </div>
<div className={classnames(styles.otpauthContainer, 'otpauth-secret')}>{secret.otpauth_url}</div>
<div className={classnames(styles.base32Container, 'base32-secret')}>{secret.base32_secret}</div> <div className={classnames(styles.base32Container, 'base32-secret')}>{secret.base32_secret}</div>
</div> </div>
<div className={styles.loginButtonContainer}> <div className={styles.loginButtonContainer}>

15
package-lock.json generated
View File

@ -358,6 +358,15 @@
"integrity": "sha512-J7nx6JzxmtT4zyvfLipYL7jNaxvlCWpyG7JhhCQ4fQyG+AGfovAHoYR55TFx+X8akfkUJYpt5JG6GPeFMjZaCQ==", "integrity": "sha512-J7nx6JzxmtT4zyvfLipYL7jNaxvlCWpyG7JhhCQ4fQyG+AGfovAHoYR55TFx+X8akfkUJYpt5JG6GPeFMjZaCQ==",
"dev": true "dev": true
}, },
"@types/node-fetch": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.1.4.tgz",
"integrity": "sha512-tR1ekaXUGpmzOcDXWU9BW73YfA2/VW1DF1FH+wlJ82BbCSnWTbdX+JkqWQXWKIGsFPnPsYadbXfNgz28g+ccWg==",
"dev": true,
"requires": {
"@types/node": "10.0.3"
}
},
"@types/nodemailer": { "@types/nodemailer": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-4.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-4.6.0.tgz",
@ -5251,6 +5260,12 @@
"resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz", "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz",
"integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA=" "integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA="
}, },
"node-fetch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
"integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==",
"dev": true
},
"nodemailer": { "nodemailer": {
"version": "4.6.4", "version": "4.6.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.4.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.4.tgz",

View File

@ -69,6 +69,7 @@
"@types/mockdate": "^2.0.0", "@types/mockdate": "^2.0.0",
"@types/mongodb": "^3.0.9", "@types/mongodb": "^3.0.9",
"@types/nedb": "^1.8.3", "@types/nedb": "^1.8.3",
"@types/node-fetch": "^2.1.4",
"@types/nodemailer": "^4.6.0", "@types/nodemailer": "^4.6.0",
"@types/nodemailer-direct-transport": "^1.0.31", "@types/nodemailer-direct-transport": "^1.0.31",
"@types/nodemailer-smtp-transport": "^2.7.4", "@types/nodemailer-smtp-transport": "^2.7.4",
@ -98,6 +99,7 @@
"jquery": "^3.2.1", "jquery": "^3.2.1",
"mocha": "^5.2.0", "mocha": "^5.2.0",
"mockdate": "^2.0.1", "mockdate": "^2.0.1",
"node-fetch": "^2.3.0",
"nodemon": "^1.18.9", "nodemon": "^1.18.9",
"nyc": "^13.1.0", "nyc": "^13.1.0",
"query-string": "^6.0.0", "query-string": "^6.0.0",

View File

@ -24,8 +24,8 @@ var tsWatcher = chokidar.watch(['server', 'shared/**/*.ts', 'node_modules'], {
// Properly cleanup server and client if ctrl-c is hit // Properly cleanup server and client if ctrl-c is hit
process.on('SIGINT', function() { process.on('SIGINT', function() {
killServer(); killServer(() => {});
killClient(); killClient(() => {});
fs.unlinkSync(ENVIRONMENT_FILENAME); fs.unlinkSync(ENVIRONMENT_FILENAME);
process.exit(); process.exit();
}); });
@ -60,21 +60,31 @@ function startClient() {
function killServer(onExit) { function killServer(onExit) {
if (serverProcess) { if (serverProcess) {
process.kill(-serverProcess.pid);
serverProcess.on('exit', () => { serverProcess.on('exit', () => {
serverProcess = undefined; serverProcess = undefined;
onExit(); onExit();
}); });
try {
process.kill(-serverProcess.pid);
} catch (e) {
console.error(e);
onExit();
}
} }
} }
function killClient(onExit) { function killClient(onExit) {
if (clientProcess) { if (clientProcess) {
process.kill(-clientProcess.pid);
clientProcess.on('exit', () => { clientProcess.on('exit', () => {
clientProcess = undefined; clientProcess = undefined;
onExit(); onExit();
}); });
try {
process.kill(-clientProcess.pid);
} catch (e) {
console.error(e);
onExit();
}
} }
} }

View File

@ -42,9 +42,9 @@ export function register(app: Express.Application,
vars: ServerVariables) { vars: ServerVariables) {
app.post(pre_validation_endpoint, app.post(pre_validation_endpoint,
get_start_validation(handler, post_validation_endpoint, vars)); post_start_validation(handler, vars));
app.post(post_validation_endpoint, app.post(post_validation_endpoint,
get_finish_validation(handler, vars)); post_finish_validation(handler, vars));
} }
function checkIdentityToken(req: Express.Request, identityToken: string) function checkIdentityToken(req: Express.Request, identityToken: string)
@ -55,7 +55,7 @@ function checkIdentityToken(req: Express.Request, identityToken: string)
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
} }
export function get_finish_validation(handler: IdentityValidable, export function post_finish_validation(handler: IdentityValidable,
vars: ServerVariables) vars: ServerVariables)
: Express.RequestHandler { : Express.RequestHandler {
@ -88,8 +88,7 @@ export function get_finish_validation(handler: IdentityValidable,
}; };
} }
export function get_start_validation(handler: IdentityValidable, export function post_start_validation(handler: IdentityValidable,
postValidationEndpoint: string,
vars: ServerVariables) vars: ServerVariables)
: Express.RequestHandler { : Express.RequestHandler {
return function (req: Express.Request, res: Express.Response) return function (req: Express.Request, res: Express.Response)

View File

@ -1,5 +1,3 @@
import { UserDataStore } from "../../../../storage/UserDataStore";
import objectPath = require("object-path"); import objectPath = require("object-path");
import u2f_common = require("../U2FCommon"); import u2f_common = require("../U2FCommon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");

View File

@ -3,11 +3,8 @@ import objectPath = require("object-path");
import u2f_common = require("../U2FCommon"); import u2f_common = require("../U2FCommon");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import express = require("express"); import express = require("express");
import { UserDataStore } from "../../../../storage/UserDataStore";
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
import { Winston } from "../../../../../../types/Dependencies";
import U2f = require("u2f"); import U2f = require("u2f");
import exceptions = require("../../../../Exceptions");
import redirect from "../../redirect"; import redirect from "../../redirect";
import ErrorReplies = require("../../../../ErrorReplies"); import ErrorReplies = require("../../../../ErrorReplies");
import { ServerVariables } from "../../../../ServerVariables"; import { ServerVariables } from "../../../../ServerVariables";

View File

@ -78,6 +78,8 @@ export default function (vars: ServerVariables) {
ErrorReplies.replyWithError401(req, res, vars.logger)) ErrorReplies.replyWithError401(req, res, vars.logger))
// The user is not yet authenticated -> 401 // The user is not yet authenticated -> 401
.catch((err) => { .catch((err) => {
// This redirect parameter is used in Kubernetes to annotate the ingress with
// the url to the authentication portal.
const redirectUrl = getRedirectParam(req); const redirectUrl = getRedirectParam(req);
if (redirectUrl) { if (redirectUrl) {
ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err);

View File

@ -30,15 +30,15 @@ function setupTotp(app: Express.Application, vars: ServerVariables) {
RequireValidatedFirstFactor.middleware(vars.logger), RequireValidatedFirstFactor.middleware(vars.logger),
TOTPSignGet.default(vars)); TOTPSignGet.default(vars));
app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, app.post(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_POST,
RequireValidatedFirstFactor.middleware(vars.logger)); RequireValidatedFirstFactor.middleware(vars.logger));
app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, app.post(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_POST,
RequireValidatedFirstFactor.middleware(vars.logger)); RequireValidatedFirstFactor.middleware(vars.logger));
IdentityCheckMiddleware.register(app, IdentityCheckMiddleware.register(app,
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_POST,
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_POST,
new TOTPRegistrationIdentityHandler(vars.logger, new TOTPRegistrationIdentityHandler(vars.logger,
vars.userDataStore, vars.totpHandler, vars.config.totp), vars.userDataStore, vars.totpHandler, vars.config.totp),
vars); vars);

View File

@ -143,7 +143,7 @@ export const SECOND_FACTOR_U2F_IDENTITY_FINISH_POST = "/api/secondfactor/u2f/ide
* *
* @apiDescription Initiates the identity validation * @apiDescription Initiates the identity validation
*/ */
export const SECOND_FACTOR_TOTP_IDENTITY_START_GET = "/api/secondfactor/totp/identity/start"; export const SECOND_FACTOR_TOTP_IDENTITY_START_POST = "/api/secondfactor/totp/identity/start";
@ -159,7 +159,7 @@ export const SECOND_FACTOR_TOTP_IDENTITY_START_GET = "/api/secondfactor/totp/ide
* @apiDescription Serves the TOTP registration page that displays the secret. * @apiDescription Serves the TOTP registration page that displays the secret.
* The secret is a QRCode and a base32 secret. * The secret is a QRCode and a base32 secret.
*/ */
export const SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET = "/api/secondfactor/totp/identity/finish"; export const SECOND_FACTOR_TOTP_IDENTITY_FINISH_POST = "/api/secondfactor/totp/identity/finish";

View File

@ -1,55 +0,0 @@
Feature: User has access restricted access to domains
@need-registered-user-john
Scenario: User john has admin access
When I visit "https://login.example.com:8080?rd=https://home.example.com:8080/"
And I login with user "john" and password "password"
And I use "REGISTERED" as TOTP token handle
And I click on "Sign in"
And I'm redirected to "https://home.example.com:8080/"
Then I have access to "https://public.example.com:8080/secret.html"
And I have access to "https://dev.example.com:8080/groups/admin/secret.html"
And I have access to "https://dev.example.com:8080/groups/dev/secret.html"
And I have access to "https://dev.example.com:8080/users/john/secret.html"
And I have access to "https://dev.example.com:8080/users/harry/secret.html"
And I have access to "https://dev.example.com:8080/users/bob/secret.html"
And I have access to "https://admin.example.com:8080/secret.html"
And I have access to "https://mx1.mail.example.com:8080/secret.html"
And I have access to "https://single_factor.example.com:8080/secret.html"
And I have no access to "https://mx2.mail.example.com:8080/secret.html"
@need-registered-user-bob
Scenario: User bob has restricted access
When I visit "https://login.example.com:8080?rd=https://home.example.com:8080/"
And I login with user "bob" and password "password"
And I use "REGISTERED" as TOTP token handle
And I click on "Sign in"
And I'm redirected to "https://home.example.com:8080/"
Then I have access to "https://public.example.com:8080/secret.html"
And I have no access to "https://dev.example.com:8080/groups/admin/secret.html"
And I have access to "https://dev.example.com:8080/groups/dev/secret.html"
And I have no access to "https://dev.example.com:8080/users/john/secret.html"
And I have no access to "https://dev.example.com:8080/users/harry/secret.html"
And I have access to "https://dev.example.com:8080/users/bob/secret.html"
And I have no access to "https://admin.example.com:8080/secret.html"
And I have access to "https://mx1.mail.example.com:8080/secret.html"
And I have access to "https://single_factor.example.com:8080/secret.html"
And I have access to "https://mx2.mail.example.com:8080/secret.html"
@need-registered-user-harry
Scenario: User harry has restricted access
When I visit "https://login.example.com:8080?rd=https://home.example.com:8080/"
And I login with user "harry" and password "password"
And I use "REGISTERED" as TOTP token handle
And I click on "Sign in"
And I'm redirected to "https://home.example.com:8080/"
Then I have access to "https://public.example.com:8080/secret.html"
And I have no access to "https://dev.example.com:8080/groups/admin/secret.html"
And I have no access to "https://dev.example.com:8080/groups/dev/secret.html"
And I have no access to "https://dev.example.com:8080/users/john/secret.html"
And I have access to "https://dev.example.com:8080/users/harry/secret.html"
And I have no access to "https://dev.example.com:8080/users/bob/secret.html"
And I have no access to "https://admin.example.com:8080/secret.html"
And I have no access to "https://mx1.mail.example.com:8080/secret.html"
And I have access to "https://single_factor.example.com:8080/secret.html"
And I have no access to "https://mx2.mail.example.com:8080/secret.html"

View File

@ -1,9 +0,0 @@
Feature: Generic tests on Authelia endpoints
Scenario: /api/verify replies with error when redirect parameter is not provided
When I query "https://authelia.example.com:8080/api/verify"
Then I get error code 401
Scenario: /api/verify redirects when redirect parameter is provided
When I query "https://authelia.example.com:8080/api/verify?rd=http://login.example.com:8080"
Then I get redirected to "http://login.example.com:8080"

View File

@ -1,38 +1,5 @@
Feature: Authentication scenarii Feature: Authentication scenarii
Scenario: User succeeds first factor
Given I visit "https://login.example.com:8080/"
When I set field "username" to "bob"
And I set field "password" to "password"
And I click on "Sign in"
Then I'm redirected to "https://login.example.com:8080/secondfactor"
Scenario: User fails first factor
Given I visit "https://login.example.com:8080/"
When I set field "username" to "john"
And I set field "password" to "bad-password"
And I click on "Sign in"
Then I get a notification of type "error" with message "Authentication failed. Please check your credentials."
Scenario: User registers TOTP secret and succeeds authentication
Given I visit "https://login.example.com:8080/"
And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0"
When I visit "https://admin.example.com:8080/secret.html"
And I'm redirected to "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 "Sec0" as TOTP token handle
And I click on "Sign in"
Then I'm redirected to "https://admin.example.com:8080/secret.html"
Scenario: User fails TOTP second factor
When I visit "https://admin.example.com:8080/secret.html"
And I'm redirected to "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 "BADTOKEN" as TOTP token
And I click on "Sign in"
Then I get a notification of type "error" with message "Authentication failed. Have you already registered your secret?"
Scenario: Logout redirects user to redirect URL given in parameter Scenario: Logout redirects user to redirect URL given in parameter
When I visit "https://login.example.com:8080/logout?rd=https://home.example.com:8080/" When I visit "https://login.example.com:8080/logout?rd=https://home.example.com:8080/"
Then I'm redirected to "https://home.example.com:8080/" Then I'm redirected to "https://home.example.com:8080/"

View File

@ -1,14 +0,0 @@
Feature: Register secret for second factor
Scenario: Register a TOTP secret with correct label and issuer
Given I visit "https://login.example.com:8080/"
And I login with user "john" and password "password"
When I register a TOTP secret called "Sec0"
Then the otpauth url has label "john" and issuer "authelia.com"
@needs-totp_issuer-config
Scenario: Register a TOTP secret with correct label and custom issuer
Given I visit "https://login.example.com:8080/"
And I login with user "john" and password "password"
When I register a TOTP secret called "Sec0"
Then the otpauth url has label "john" and issuer "custom.com"

View File

@ -1,39 +0,0 @@
Feature: User is able to reset his password
Scenario: User is redirected to password reset page
Given I'm on "https://login.example.com:8080"
When I click on the link "Forgot password?"
Then I'm redirected to "https://login.example.com:8080/password-reset/request"
Scenario: User get an email with a link to reset password
Given I'm on "https://login.example.com:8080/password-reset/request"
When I set field "username" to "james"
And I click on "Reset Password"
Then I get a notification of type "success" with message "An email has been sent to you. Follow the link to change your password."
Scenario: Request password for unexisting user should behave like existing user
Given I'm on "https://login.example.com:8080/password-reset/request"
When I set field "username" to "fake_user"
And I click on "Reset Password"
Then I get a notification of type "success" with message "An email has been sent to you. Follow the link to change your password."
Scenario: User resets his password
Given I'm on "https://login.example.com:8080/password-reset/request"
And I set field "username" to "james"
And I click on "Reset Password"
When I click on the link of the email
And I set field "password1" to "newpassword"
And I set field "password2" to "newpassword"
And I click on "Reset Password"
Then I'm redirected to "https://login.example.com:8080/"
Scenario: User does not confirm new password
Given I'm on "https://login.example.com:8080/password-reset/request"
And I set field "username" to "james"
And I click on "Reset Password"
When I click on the link of the email
And I set field "password1" to "newpassword"
And I set field "password2" to "newpassword2"
And I click on "Reset Password"
Then I get a notification of type "warning" with message "The passwords are different."

View File

@ -1,16 +0,0 @@
Feature: Non authenticated users have no access to certain pages
Scenario: Anonymous user has no access to protected pages
Then I get the following status code when requesting:
| url | code | method |
| https://login.example.com:8080/secondfactor | 401 | GET |
| https://login.example.com:8080/secondfactor/u2f/identity/start | 401 | GET |
| https://login.example.com:8080/secondfactor/u2f/identity/finish | 401 | GET |
| https://login.example.com:8080/secondfactor/totp/identity/start | 401 | GET |
| https://login.example.com:8080/secondfactor/totp/identity/finish | 401 | GET |
| https://login.example.com:8080/loggedin | 401 | GET |
| https://login.example.com:8080/api/totp | 401 | POST |
| https://login.example.com:8080/api/u2f/sign_request | 401 | GET |
| https://login.example.com:8080/api/u2f/sign | 401 | POST |
| https://login.example.com:8080/api/u2f/register_request | 401 | GET |
| https://login.example.com:8080/api/u2f/register | 401 | POST |

View File

@ -1,20 +0,0 @@
@needs-inactivity-config
Feature: Session is closed after a certain amount of time
@need-authenticated-user-john
Scenario: An authenticated user is disconnected after a certain inactivity period
Given I have access to "https://public.example.com:8080/secret.html"
When I sleep for 6 seconds
And I visit "https://public.example.com:8080/secret.html"
Then I'm redirected to "https://login.example.com:8080/?rd=https://public.example.com:8080/secret.html"
@need-authenticated-user-john
Scenario: An authenticated user is disconnected after session expiration period
Given I have access to "https://public.example.com:8080/secret.html"
When I sleep for 4 seconds
And I visit "https://public.example.com:8080/secret.html"
And I sleep for 4 seconds
And I visit "https://public.example.com:8080/secret.html"
And I sleep for 4 seconds
And I visit "https://public.example.com:8080/secret.html"
Then I'm redirected to "https://login.example.com:8080/?rd=https://public.example.com:8080/secret.html"

View File

@ -1,4 +1,4 @@
import SeleniumWebdriver, { ThenableWebDriver, WebDriver } from "selenium-webdriver"; import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
import Assert = require("assert"); import Assert = require("assert");
export default async function(driver: WebDriver, type: string, message: string) { export default async function(driver: WebDriver, type: string, message: string) {

View File

@ -0,0 +1,10 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
// Verify if the current page contains "This is a very important secret!".
export default async function(driver: WebDriver, timeout: number = 5000) {
const el = await driver.wait(
SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.tagName('body')), timeout);
await driver.wait(
SeleniumWebdriver.until.elementTextContains(el, "This is a very important secret!"), timeout);
}

View File

@ -18,7 +18,6 @@ function AutheliaSuiteBase(description: string, configPath: string,
} }
return context('Suite: ' + description, function(this: Mocha.ISuiteCallbackContext) { return context('Suite: ' + description, function(this: Mocha.ISuiteCallbackContext) {
WithDriver.call(this);
cb.call(this); cb.call(this);
}); });
} }

View File

@ -2,22 +2,30 @@ require("chromedriver");
import chrome from 'selenium-webdriver/chrome'; import chrome from 'selenium-webdriver/chrome';
import SeleniumWebdriver from "selenium-webdriver"; import SeleniumWebdriver from "selenium-webdriver";
export default function() { export default function(forEach: boolean = false) {
let options = new chrome.Options(); let options = new chrome.Options();
if (process.env['HEADLESS'] == 'y') { if (process.env['HEADLESS'] == 'y') {
options = options.headless(); options = options.headless();
} }
beforeEach(function() { function beforeBlock(this: Mocha.IHookCallbackContext) {
const driver = new SeleniumWebdriver.Builder() const driver = new SeleniumWebdriver.Builder()
.forBrowser("chrome") .forBrowser("chrome")
.setChromeOptions(options) .setChromeOptions(options)
.build(); .build();
this.driver = driver; this.driver = driver;
}); }
afterEach(function() { function afterBlock(this: Mocha.IHookCallbackContext) {
this.driver.quit(); return this.driver.quit();
}); }
if (forEach) {
beforeEach(beforeBlock);
afterEach(afterBlock);
} else {
before(beforeBlock);
after(afterBlock);
}
} }

View File

@ -0,0 +1,52 @@
import Request from 'request-promise';
import Fetch from 'node-fetch';
import Assert from 'assert';
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) {
try {
await Request.get(url, {
json: true,
rejectUnauthorized: false,
});
throw new Error('No response');
} catch (e) {
if (e instanceof StatusCodeError) {
Assert.equal(e.statusCode, 401);
return;
}
}
return;
}
export async function POST_Expect401(url: string, body?: any) {
try {
await Request.post(url, {
json: true,
rejectUnauthorized: false,
body
});
throw new Error('No response');
} catch (e) {
if (e instanceof StatusCodeError) {
Assert.equal(e.statusCode, 401);
return;
}
}
return;
}
export async function GET_ExpectRedirect(url: string, redirectionUrl: string) {
const response = await Fetch(url, {redirect: 'manual'});
if (response.status == 302) {
const body = await response.text();
Assert.equal(body, 'Found. Redirecting to ' + redirectionUrl);
return;
}
throw new Error('No redirect');
}

View File

@ -1,10 +1,13 @@
import AutheliaSuite from "../../helpers/context/AutheliaSuite"; import AutheliaSuite from "../../helpers/context/AutheliaSuite";
import MongoConnectionRecovery from "./scenarii/MongoConnectionRecovery"; import MongoConnectionRecovery from "./scenarii/MongoConnectionRecovery";
import EnforceInternalRedirectionsOnly from "./scenarii/EnforceInternalRedirectionsOnly"; import EnforceInternalRedirectionsOnly from "./scenarii/EnforceInternalRedirectionsOnly";
import AccessControl from "./scenarii/AccessControl";
AutheliaSuite('Complete configuration', __dirname + '/config.yml', function() { AutheliaSuite('Complete configuration', __dirname + '/config.yml', function() {
this.timeout(10000); this.timeout(10000);
describe('Access control', AccessControl);
describe('Mongo broken connection recovery', MongoConnectionRecovery); describe('Mongo broken connection recovery', MongoConnectionRecovery);
describe('Enforce internal redirections only', EnforceInternalRedirectionsOnly); describe('Enforce internal redirections only', EnforceInternalRedirectionsOnly);
}); });

View File

@ -0,0 +1,107 @@
import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp";
import VisitPage from "../../../helpers/VisitPage";
import ObserveSecret from "../../../helpers/assertions/ObserveSecret";
import WithDriver from "../../../helpers/context/WithDriver";
import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick";
import ValidateTotp from "../../../helpers/ValidateTotp";
import WaitRedirected from "../../../helpers/WaitRedirected";
import Logout from "../../../helpers/Logout";
async function ShouldHaveAccessTo(url: string) {
it('should have access to ' + url, async function() {
await VisitPage(this.driver, url);
await ObserveSecret(this.driver);
})
}
async function ShouldNotHaveAccessTo(url: string) {
it('should not have access to ' + url, async function() {
await this.driver.get(url);
await WaitRedirected(this.driver, 'https://login.example.com:8080/');
})
}
// we verify that the user has only access to want he is granted to.
export default function() {
// We ensure that bob has access to what he is granted to
describe('Permissions of user john', function() {
after(async function() {
await Logout(this.driver);
})
WithDriver();
before(async function() {
const secret = await LoginAndRegisterTotp(this.driver, "john", true);
await VisitPage(this.driver, 'https://login.example.com:8080/');
await FillLoginPageAndClick(this.driver, 'john', 'password', false);
await ValidateTotp(this.driver, secret);
})
ShouldHaveAccessTo('https://public.example.com:8080/secret.html');
ShouldHaveAccessTo('https://dev.example.com:8080/groups/admin/secret.html');
ShouldHaveAccessTo('https://dev.example.com:8080/groups/dev/secret.html');
ShouldHaveAccessTo('https://dev.example.com:8080/users/john/secret.html');
ShouldHaveAccessTo('https://dev.example.com:8080/users/harry/secret.html');
ShouldHaveAccessTo('https://dev.example.com:8080/users/bob/secret.html');
ShouldHaveAccessTo('https://admin.example.com:8080/secret.html');
ShouldHaveAccessTo('https://mx1.mail.example.com:8080/secret.html');
ShouldHaveAccessTo('https://single_factor.example.com:8080/secret.html');
ShouldNotHaveAccessTo('https://mx2.mail.example.com:8080/secret.html');
})
// We ensure that bob has access to what he is granted to
describe('Permissions of user bob', function() {
after(async function() {
await Logout(this.driver);
})
WithDriver();
before(async function() {
const secret = await LoginAndRegisterTotp(this.driver, "bob", true);
await VisitPage(this.driver, 'https://login.example.com:8080/');
await FillLoginPageAndClick(this.driver, 'bob', 'password', false);
await ValidateTotp(this.driver, secret);
})
ShouldHaveAccessTo('https://public.example.com:8080/secret.html');
ShouldNotHaveAccessTo('https://dev.example.com:8080/groups/admin/secret.html');
ShouldHaveAccessTo('https://dev.example.com:8080/groups/dev/secret.html');
ShouldNotHaveAccessTo('https://dev.example.com:8080/users/john/secret.html');
ShouldNotHaveAccessTo('https://dev.example.com:8080/users/harry/secret.html');
ShouldHaveAccessTo('https://dev.example.com:8080/users/bob/secret.html');
ShouldNotHaveAccessTo('https://admin.example.com:8080/secret.html');
ShouldHaveAccessTo('https://mx1.mail.example.com:8080/secret.html');
ShouldHaveAccessTo('https://single_factor.example.com:8080/secret.html');
ShouldHaveAccessTo('https://mx2.mail.example.com:8080/secret.html');
})
// We ensure that harry has access to what he is granted to
describe('Permissions of user harry', function() {
after(async function() {
await Logout(this.driver);
})
WithDriver();
before(async function() {
const secret = await LoginAndRegisterTotp(this.driver, "harry", true);
await VisitPage(this.driver, 'https://login.example.com:8080/');
await FillLoginPageAndClick(this.driver, 'harry', 'password', false);
await ValidateTotp(this.driver, secret);
})
ShouldHaveAccessTo('https://public.example.com:8080/secret.html');
ShouldNotHaveAccessTo('https://dev.example.com:8080/groups/admin/secret.html');
ShouldNotHaveAccessTo('https://dev.example.com:8080/groups/dev/secret.html');
ShouldNotHaveAccessTo('https://dev.example.com:8080/users/john/secret.html');
ShouldHaveAccessTo('https://dev.example.com:8080/users/harry/secret.html');
ShouldNotHaveAccessTo('https://dev.example.com:8080/users/bob/secret.html');
ShouldNotHaveAccessTo('https://admin.example.com:8080/secret.html');
ShouldNotHaveAccessTo('https://mx1.mail.example.com:8080/secret.html');
ShouldHaveAccessTo('https://single_factor.example.com:8080/secret.html');
ShouldNotHaveAccessTo('https://mx2.mail.example.com:8080/secret.html');
})
}

View File

@ -5,6 +5,7 @@ import ValidateTotp from "../../../helpers/ValidateTotp";
import Logout from "../../../helpers/Logout"; import Logout from "../../../helpers/Logout";
import WaitRedirected from "../../../helpers/WaitRedirected"; import WaitRedirected from "../../../helpers/WaitRedirected";
import IsAlreadyAuthenticatedStage from "../../../helpers/IsAlreadyAuthenticatedStage"; import IsAlreadyAuthenticatedStage from "../../../helpers/IsAlreadyAuthenticatedStage";
import WithDriver from "../../../helpers/context/WithDriver";
/* /*
* Authelia should not be vulnerable to open redirection. Otherwise it would aid an * Authelia should not be vulnerable to open redirection. Otherwise it would aid an
@ -14,6 +15,7 @@ import IsAlreadyAuthenticatedStage from "../../../helpers/IsAlreadyAuthenticated
* the URL is pointing to an external domain. * the URL is pointing to an external domain.
*/ */
export default function() { export default function() {
WithDriver(true);
describe("Only redirection to a subdomain of the protected domain should be allowed", function() { describe("Only redirection to a subdomain of the protected domain should be allowed", function() {
this.timeout(10000); this.timeout(10000);
let secret: string; let secret: string;
@ -44,18 +46,22 @@ export default function() {
}); });
} }
describe('blocked redirection', function() { describe('Cannot redirect to https://www.google.fr', function() {
// Do not redirect to another domain than example.com // Do not redirect to another domain than example.com
CannotRedirectTo("https://www.google.fr"); CannotRedirectTo("https://www.google.fr");
});
// Do not redirect to rogue domain describe('Cannot redirect to https://public.example.com.a:8080', function() {
// Do not redirect to another domain than example.com
CannotRedirectTo("https://public.example.com.a:8080"); CannotRedirectTo("https://public.example.com.a:8080");
});
describe('Cannot redirect to http://public.example.com:8080', function() {
// Do not redirect to http website // Do not redirect to http website
CannotRedirectTo("http://public.example.com:8080"); CannotRedirectTo("http://public.example.com:8080");
}); });
describe('allowed redirection', function() { describe('Can redirect to https://public.example.com:8080/', function() {
// Can redirect to any subdomain of the domain protected by Authelia. // Can redirect to any subdomain of the domain protected by Authelia.
CanRedirectTo("https://public.example.com:8080/"); CanRedirectTo("https://public.example.com:8080/");
}); });

View File

@ -1,8 +1,16 @@
import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp";
import FullLogin from "../../../helpers/FullLogin"; import FullLogin from "../../../helpers/FullLogin";
import child_process from 'child_process'; import child_process from 'child_process';
import WithDriver from "../../../helpers/context/WithDriver";
import Logout from "../../../helpers/Logout";
export default function() { export default function() {
after(async function() {
await Logout(this.driver);
})
WithDriver();
it("should be able to login after mongo restarts", async function() { it("should be able to login after mongo restarts", async function() {
this.timeout(30000); this.timeout(30000);

View File

@ -14,6 +14,7 @@ session:
secret: unsecure_session_secret secret: unsecure_session_secret
domain: example.com domain: example.com
inactivity: 5000 inactivity: 5000
expiration: 8000
# Configuration of the storage backend used to store data and secrets. i.e. totp data # Configuration of the storage backend used to store data and secrets. i.e. totp data
storage: storage:

View File

@ -7,6 +7,8 @@ import RegisterTotp from './scenarii/RegisterTotp';
import ResetPassword from './scenarii/ResetPassword'; import ResetPassword from './scenarii/ResetPassword';
import TOTPValidation from './scenarii/TOTPValidation'; import TOTPValidation from './scenarii/TOTPValidation';
import Inactivity from './scenarii/Inactivity'; import Inactivity from './scenarii/Inactivity';
import BackendProtection from './scenarii/BackendProtection';
import VerifyEndpoint from './scenarii/VerifyEndpoint';
const execAsync = Bluebird.promisify(ChildProcess.exec); const execAsync = Bluebird.promisify(ChildProcess.exec);
@ -16,6 +18,9 @@ AutheliaSuite('Minimal configuration', __dirname + '/config.yml', function() {
return execAsync("cp users_database.example.yml users_database.yml"); return execAsync("cp users_database.example.yml users_database.yml");
}); });
describe('Backend protection', BackendProtection);
describe('Verify API endpoint', VerifyEndpoint);
describe('Bad password', BadPassword); describe('Bad password', BadPassword);
describe('Reset password', ResetPassword); describe('Reset password', ResetPassword);

View File

@ -0,0 +1,45 @@
import { POST_Expect401, GET_Expect401 } from "../../../helpers/utils/Requests";
export default function() {
// POST
it('should return 401 error when posting to https://login.example.com:8080/api/totp', async function() {
await POST_Expect401('https://login.example.com:8080/api/totp', { token: 'MALICIOUS_TOKEN' });
});
it('should return 401 error when posting to https://login.example.com:8080/api/u2f/sign', async function() {
await POST_Expect401('https://login.example.com:8080/api/u2f/sign');
});
it('should return 401 error when posting to https://login.example.com:8080/api/u2f/register', async function() {
await POST_Expect401('https://login.example.com:8080/api/u2f/register');
});
// GET
it('should return 401 error on GET to https://login.example.com:8080/api/u2f/sign_request', async function() {
await GET_Expect401('https://login.example.com:8080/api/u2f/sign_request');
});
it('should return 401 error on GET to https://login.example.com:8080/api/u2f/register_request', async function() {
await GET_Expect401('https://login.example.com:8080/api/u2f/register_request');
});
describe('Identity validation endpoints blocked to unauthenticated users', function() {
it('should return 401 error on POST to https://login.example.com:8080/api/secondfactor/u2f/identity/start', async function() {
await POST_Expect401('https://login.example.com:8080/api/secondfactor/u2f/identity/start');
});
it('should return 401 error on POST to https://login.example.com:8080/api/secondfactor/u2f/identity/finish', async function() {
await POST_Expect401('https://login.example.com:8080/api/secondfactor/u2f/identity/finish');
});
it('should return 401 error on POST to https://login.example.com:8080/api/secondfactor/totp/identity/start', async function() {
await POST_Expect401('https://login.example.com:8080/api/secondfactor/totp/identity/start');
});
it('should return 401 error on POST to https://login.example.com:8080/api/secondfactor/totp/identity/finish', async function() {
await POST_Expect401('https://login.example.com:8080/api/secondfactor/totp/identity/finish');
});
});
}

View File

@ -4,16 +4,17 @@ import VisitPage from "../../../helpers/VisitPage";
import FillLoginPageWithUserAndPasswordAndClick from "../../../helpers/FillLoginPageAndClick"; import FillLoginPageWithUserAndPasswordAndClick from "../../../helpers/FillLoginPageAndClick";
import ValidateTotp from "../../../helpers/ValidateTotp"; import ValidateTotp from "../../../helpers/ValidateTotp";
import WaitRedirected from "../../../helpers/WaitRedirected"; import WaitRedirected from "../../../helpers/WaitRedirected";
import { WebDriver } from "selenium-webdriver";
export default function(this: Mocha.ISuiteCallbackContext) { export default function(this: Mocha.ISuiteCallbackContext) {
this.timeout(15000); this.timeout(20000);
beforeEach(async function() { beforeEach(async function() {
this.secret = await LoginAndRegisterTotp(this.driver, "john", true); this.secret = await LoginAndRegisterTotp(this.driver, "john", true);
}); });
it("should disconnect user after inactivity period", async function() { it("should disconnect user after inactivity period", async function() {
const driver = this.driver; const driver = this.driver as WebDriver;
await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html"); await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', false); await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', false);
await ValidateTotp(driver, this.secret); await ValidateTotp(driver, this.secret);
@ -24,8 +25,28 @@ export default function(this: Mocha.ISuiteCallbackContext) {
await WaitRedirected(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html"); await WaitRedirected(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
}); });
it('should disconnect user after cookie expiration', async function() {
const driver = this.driver as WebDriver;
await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', false);
await ValidateTotp(driver, this.secret);
await WaitRedirected(driver, "https://admin.example.com:8080/secret.html");
await VisitPage(driver, "https://home.example.com:8080/");
await driver.sleep(4000);
await driver.get("https://admin.example.com:8080/secret.html");
await driver.sleep(2000);
await driver.get("https://admin.example.com:8080/secret.html");
await driver.sleep(2000);
await driver.get("https://admin.example.com:8080/secret.html");
await WaitRedirected(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
});
describe('With remember me checkbox checked', function() {
it("should keep user logged in after inactivity period", async function() { it("should keep user logged in after inactivity period", async function() {
const driver = this.driver; const driver = this.driver as WebDriver;
await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html"); await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', true); await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', true);
await ValidateTotp(driver, this.secret); await ValidateTotp(driver, this.secret);
@ -35,4 +56,5 @@ export default function(this: Mocha.ISuiteCallbackContext) {
await driver.get("https://admin.example.com:8080/secret.html"); await driver.get("https://admin.example.com:8080/secret.html");
await WaitRedirected(driver, "https://admin.example.com:8080/secret.html"); await WaitRedirected(driver, "https://admin.example.com:8080/secret.html");
}); });
});
} }

View File

@ -1,4 +1,5 @@
import SeleniumWebdriver from "selenium-webdriver"; import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
import Assert from 'assert';
import LoginAndRegisterTotp from '../../../helpers/LoginAndRegisterTotp'; import LoginAndRegisterTotp from '../../../helpers/LoginAndRegisterTotp';
/** /**
@ -26,5 +27,18 @@ export default function() {
SeleniumWebdriver.By.className("base32-secret")), SeleniumWebdriver.By.className("base32-secret")),
5000); 5000);
}); });
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);
const otpauthUrl = await el.getAttribute('innerText');
const label = 'john';
const issuer = 'example.com';
Assert(new RegExp(`^otpauth://totp/${label}\\?secret=[A-Z0-9]+&issuer=${issuer}$`).test(otpauthUrl));
})
}); });
}; };

View File

@ -8,6 +8,7 @@ import FillField from "../../../helpers/FillField";
import {GetLinkFromEmail} from "../../../helpers/GetIdentityLink"; import {GetLinkFromEmail} from "../../../helpers/GetIdentityLink";
import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick"; import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick";
import IsSecondFactorStage from "../../../helpers/IsSecondFactorStage"; import IsSecondFactorStage from "../../../helpers/IsSecondFactorStage";
import SeeNotification from '../../../helpers/SeeNotification';
export default function() { export default function() {
it("should reset password for john", async function() { it("should reset password for john", async function() {
@ -16,6 +17,7 @@ export default function() {
await WaitRedirected(this.driver, "https://login.example.com:8080/forgot-password"); await WaitRedirected(this.driver, "https://login.example.com:8080/forgot-password");
await FillField(this.driver, "username", "john"); await FillField(this.driver, "username", "john");
await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button')); await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button'));
await WaitRedirected(this.driver, 'https://login.example.com:8080/confirmation-sent');
await this.driver.sleep(500); // Simulate the time it takes to receive the e-mail. await this.driver.sleep(500); // Simulate the time it takes to receive the e-mail.
const link = await GetLinkFromEmail(); const link = await GetLinkFromEmail();
@ -25,6 +27,36 @@ export default function() {
await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button')); await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button'));
await WaitRedirected(this.driver, "https://login.example.com:8080/"); await WaitRedirected(this.driver, "https://login.example.com:8080/");
await FillLoginPageAndClick(this.driver, "john", "newpass"); await FillLoginPageAndClick(this.driver, "john", "newpass");
// The user reaches the second factor page using the new password.
await IsSecondFactorStage(this.driver); await IsSecondFactorStage(this.driver);
}); });
it("should persuade reset password is initiated for unknown user", async function() {
await VisitPage(this.driver, "https://login.example.com:8080/");
await ClickOnLink(this.driver, "Forgot password\?");
await WaitRedirected(this.driver, "https://login.example.com:8080/forgot-password");
await FillField(this.driver, "username", "unknown");
await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button'));
// The malicious user thinks the confirmation has been sent.
await WaitRedirected(this.driver, 'https://login.example.com:8080/confirmation-sent');
});
it("should notify passwords are different in reset form", async function() {
await VisitPage(this.driver, "https://login.example.com:8080/");
await ClickOnLink(this.driver, "Forgot password\?");
await WaitRedirected(this.driver, "https://login.example.com:8080/forgot-password");
await FillField(this.driver, "username", "john");
await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button'));
await WaitRedirected(this.driver, 'https://login.example.com:8080/confirmation-sent');
await this.driver.sleep(500); // Simulate the time it takes to receive the e-mail.
const link = await GetLinkFromEmail();
await VisitPage(this.driver, link);
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.");
});
} }

View File

@ -0,0 +1,16 @@
import { GET_Expect401, GET_ExpectRedirect } from "../../../helpers/utils/Requests";
export default function() {
describe('Query without authenticated cookie', function() {
it('should get a 401 on GET to https://authelia.example.com:8080/api/verify', async function() {
await GET_Expect401('https://login.example.com:8080/api/verify');
});
describe('Parameter `rd` required by Kubernetes ingress controller', async function() {
it('should redirect to https://login.example.com:8080', async function() {
await GET_ExpectRedirect('https://login.example.com:8080/api/verify?rd=https://login.example.com:8080',
'https://login.example.com:8080');
});
});
});
}