Merge pull request #293 from clems4ever/closed-redirection

Fix open redirection vulnerability.
This commit is contained in:
Clément Michaud 2018-11-17 18:04:33 +01:00 committed by GitHub
commit d898fa2c0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 248 additions and 105 deletions

View File

@ -9,6 +9,9 @@ module.exports = function (grunt) {
}, },
"env-test-client-unit": { "env-test-client-unit": {
TS_NODE_PROJECT: "client/tsconfig.json" TS_NODE_PROJECT: "client/tsconfig.json"
},
"env-test-shared-unit": {
TS_NODE_PROJECT: "server/tsconfig.json"
} }
}, },
run: { run: {
@ -37,6 +40,10 @@ module.exports = function (grunt) {
cmd: "./node_modules/.bin/mocha", cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'server/src/**/*.spec.ts'] args: ['--colors', '--require', 'ts-node/register', 'server/src/**/*.spec.ts']
}, },
"test-shared-unit": {
cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'shared/**/*.spec.ts']
},
"test-client-unit": { "test-client-unit": {
cmd: "./node_modules/.bin/mocha", cmd: "./node_modules/.bin/mocha",
args: ['--colors', '--require', 'ts-node/register', 'client/test/**/*.test.ts'] args: ['--colors', '--require', 'ts-node/register', 'client/test/**/*.test.ts']
@ -193,8 +200,9 @@ module.exports = function (grunt) {
grunt.registerTask('compile-client', ['run:lint-client', 'run:compile-client']) grunt.registerTask('compile-client', ['run:lint-client', 'run:compile-client'])
grunt.registerTask('test-server', ['env:env-test-server-unit', 'run:test-server-unit']) grunt.registerTask('test-server', ['env:env-test-server-unit', 'run:test-server-unit'])
grunt.registerTask('test-shared', ['env:env-test-shared-unit', 'run:test-shared-unit'])
grunt.registerTask('test-client', ['env:env-test-client-unit', 'run:test-client-unit']) grunt.registerTask('test-client', ['env:env-test-client-unit', 'run:test-client-unit'])
grunt.registerTask('test-unit', ['test-server', 'test-client']); grunt.registerTask('test-unit', ['test-server', 'test-client', 'test-shared']);
grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config', 'run:test-complete-config', 'run:test-inactivity']); grunt.registerTask('test-int', ['run:test-cucumber', 'run:test-minimal-config', 'run:test-complete-config', 'run:test-inactivity']);
grunt.registerTask('copy-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']); grunt.registerTask('copy-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']);

View File

@ -0,0 +1,10 @@
import { BelongToDomain } from "../../../shared/BelongToDomain";
export function SafeRedirect(url: string, cb: () => void): void {
const domain = window.location.hostname.split(".").slice(-2).join(".");
if (url.startsWith("/") || BelongToDomain(url, domain)) {
window.location.href = url;
return;
}
cb();
}

View File

@ -6,6 +6,7 @@ import { QueryParametersRetriever } from "../QueryParametersRetriever";
import Constants = require("../../../../shared/constants"); import Constants = require("../../../../shared/constants");
import Endpoints = require("../../../../shared/api"); import Endpoints = require("../../../../shared/api");
import UserMessages = require("../../../../shared/UserMessages"); import UserMessages = require("../../../../shared/UserMessages");
import { SafeRedirect } from "../SafeRedirect";
export default function (window: Window, $: JQueryStatic, export default function (window: Window, $: JQueryStatic,
firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) { firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) {
@ -28,7 +29,9 @@ export default function (window: Window, $: JQueryStatic,
} }
function onFirstFactorSuccess(redirectUrl: string) { function onFirstFactorSuccess(redirectUrl: string) {
window.location.href = redirectUrl; SafeRedirect(redirectUrl, () => {
notifier.error("Cannot redirect to an external domain.");
});
} }
function onFirstFactorFailure(err: Error) { function onFirstFactorFailure(err: Error) {

View File

@ -1,7 +1,6 @@
import U2f = require("u2f"); import U2f = require("u2f");
import U2fApi from "u2f-api"; import U2fApi from "u2f-api";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import { SignMessage } from "../../../../shared/SignMessage";
import Endpoints = require("../../../../shared/api"); import Endpoints = require("../../../../shared/api");
import UserMessages = require("../../../../shared/UserMessages"); import UserMessages = require("../../../../shared/UserMessages");
import { INotifier } from "../INotifier"; import { INotifier } from "../INotifier";
@ -31,24 +30,13 @@ function finishU2fAuthentication(responseData: U2fApi.SignResponse,
}); });
} }
function startU2fAuthentication($: JQueryStatic, notifier: INotifier) export function validate($: JQueryStatic): BluebirdPromise<string> {
: BluebirdPromise<string> {
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, return GetPromised($, Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {},
undefined, "json") undefined, "json")
.then(function (signRequest: U2f.Request) { .then(function (signRequest: U2f.Request) {
notifier.info(UserMessages.PLEASE_TOUCH_TOKEN);
return U2fApi.sign(signRequest, 60); return U2fApi.sign(signRequest, 60);
}) })
.then(function (signResponse: U2fApi.SignResponse) { .then(function (signResponse: U2fApi.SignResponse) {
return finishU2fAuthentication(signResponse, $); return finishU2fAuthentication(signResponse, $);
}); });
} }
export function validate($: JQueryStatic, notifier: INotifier) {
return startU2fAuthentication($, notifier)
.catch(function (err: Error) {
notifier.error(UserMessages.U2F_TRANSACTION_FINISH_FAILED);
return BluebirdPromise.reject(err);
});
}

View File

@ -3,38 +3,44 @@ import U2FValidator = require("./U2FValidator");
import ClientConstants = require("./constants"); import ClientConstants = require("./constants");
import { Notifier } from "../Notifier"; import { Notifier } from "../Notifier";
import { QueryParametersRetriever } from "../QueryParametersRetriever"; import { QueryParametersRetriever } from "../QueryParametersRetriever";
import Endpoints = require("../../../../shared/api");
import ServerConstants = require("../../../../shared/constants");
import UserMessages = require("../../../../shared/UserMessages"); import UserMessages = require("../../../../shared/UserMessages");
import SharedConstants = require("../../../../shared/constants"); import SharedConstants = require("../../../../shared/constants");
import { SafeRedirect } from "../SafeRedirect";
export default function (window: Window, $: JQueryStatic) { export default function (window: Window, $: JQueryStatic) {
const notifierTotp = new Notifier(".notification-totp", $); const notifier = new Notifier(".notification", $);
const notifierU2f = new Notifier(".notification-u2f", $);
function onAuthenticationSuccess(serverRedirectUrl: string, notifier: Notifier) { function onAuthenticationSuccess(serverRedirectUrl: string) {
if (QueryParametersRetriever.get(SharedConstants.REDIRECT_QUERY_PARAM)) const queryRedirectUrl = QueryParametersRetriever.get(SharedConstants.REDIRECT_QUERY_PARAM);
window.location.href = QueryParametersRetriever.get(SharedConstants.REDIRECT_QUERY_PARAM); if (queryRedirectUrl) {
else if (serverRedirectUrl) SafeRedirect(queryRedirectUrl, () => {
window.location.href = serverRedirectUrl; notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
else });
} else if (serverRedirectUrl) {
SafeRedirect(serverRedirectUrl, () => {
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
});
} else {
notifier.success(UserMessages.AUTHENTICATION_SUCCEEDED); notifier.success(UserMessages.AUTHENTICATION_SUCCEEDED);
}
} }
function onSecondFactorTotpSuccess(redirectUrl: string) { function onSecondFactorTotpSuccess(redirectUrl: string) {
onAuthenticationSuccess(redirectUrl, notifierTotp); onAuthenticationSuccess(redirectUrl);
} }
function onSecondFactorTotpFailure(err: Error) { function onSecondFactorTotpFailure(err: Error) {
notifierTotp.error(UserMessages.AUTHENTICATION_TOTP_FAILED); notifier.error(UserMessages.AUTHENTICATION_TOTP_FAILED);
} }
function onU2fAuthenticationSuccess(redirectUrl: string) { function onU2fAuthenticationSuccess(redirectUrl: string) {
onAuthenticationSuccess(redirectUrl, notifierU2f); onAuthenticationSuccess(redirectUrl);
} }
function onU2fAuthenticationFailure() { function onU2fAuthenticationFailure() {
notifierU2f.error(UserMessages.AUTHENTICATION_U2F_FAILED); // TODO(clems4ever): we should not display this error message until a device
// is registered.
// notifier.error(UserMessages.AUTHENTICATION_U2F_FAILED);
} }
function onTOTPFormSubmitted(): boolean { function onTOTPFormSubmitted(): boolean {
@ -47,7 +53,7 @@ export default function (window: Window, $: JQueryStatic) {
$(window.document).ready(function () { $(window.document).ready(function () {
$(ClientConstants.TOTP_FORM_SELECTOR).on("submit", onTOTPFormSubmitted); $(ClientConstants.TOTP_FORM_SELECTOR).on("submit", onTOTPFormSubmitted);
U2FValidator.validate($, notifierU2f) U2FValidator.validate($)
.then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure); .then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure);
}); });
} }

View File

@ -8,6 +8,7 @@ import Endpoints = require("../../../../shared/api");
import UserMessages = require("../../../../shared/UserMessages"); import UserMessages = require("../../../../shared/UserMessages");
import { RedirectionMessage } from "../../../../shared/RedirectionMessage"; import { RedirectionMessage } from "../../../../shared/RedirectionMessage";
import { ErrorMessage } from "../../../../shared/ErrorMessage"; import { ErrorMessage } from "../../../../shared/ErrorMessage";
import { SafeRedirect } from "../SafeRedirect";
export default function (window: Window, $: JQueryStatic) { export default function (window: Window, $: JQueryStatic) {
const notifier = new Notifier(".notification", $); const notifier = new Notifier(".notification", $);
@ -44,7 +45,9 @@ export default function (window: Window, $: JQueryStatic) {
$(document).ready(function () { $(document).ready(function () {
requestRegistration() requestRegistration()
.then((redirectionUrl: string) => { .then((redirectionUrl: string) => {
document.location.href = redirectionUrl; SafeRedirect(redirectionUrl, () => {
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
});
}) })
.catch((err) => { .catch((err) => {
onRegisterFailure(err); onRegisterFailure(err);

View File

@ -7,45 +7,61 @@ import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler
import Constants = require("../../../../../shared/constants"); import Constants = require("../../../../../shared/constants");
import Util = require("util"); import Util = require("util");
import { ServerVariables } from "../../ServerVariables"; import { ServerVariables } from "../../ServerVariables";
import { SafeRedirector } from "../../utils/SafeRedirection";
function getRedirectParam(req: express.Request) { function getRedirectParam(
req: express.Request) {
return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined" return req.query[Constants.REDIRECT_QUERY_PARAM] != "undefined"
? req.query[Constants.REDIRECT_QUERY_PARAM] ? req.query[Constants.REDIRECT_QUERY_PARAM]
: undefined; : undefined;
} }
function redirectToSecondFactorPage(req: express.Request, res: express.Response) { function redirectToSecondFactorPage(
req: express.Request,
res: express.Response) {
const redirectUrl = getRedirectParam(req); const redirectUrl = getRedirectParam(req);
if (!redirectUrl) if (!redirectUrl)
res.redirect(Endpoints.SECOND_FACTOR_GET); res.redirect(Endpoints.SECOND_FACTOR_GET);
else else
res.redirect(Util.format("%s?%s=%s", Endpoints.SECOND_FACTOR_GET, res.redirect(
Constants.REDIRECT_QUERY_PARAM, Util.format("%s?%s=%s",
redirectUrl)); Endpoints.SECOND_FACTOR_GET,
Constants.REDIRECT_QUERY_PARAM,
redirectUrl));
} }
function redirectToService(req: express.Request, res: express.Response) { function redirectToService(
req: express.Request,
res: express.Response,
redirector: SafeRedirector) {
const redirectUrl = getRedirectParam(req); const redirectUrl = getRedirectParam(req);
if (!redirectUrl) if (!redirectUrl) {
res.redirect(Endpoints.LOGGED_IN); res.redirect(Endpoints.LOGGED_IN);
else } else {
res.redirect(redirectUrl); redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN);
}
} }
function renderFirstFactor(res: express.Response) { function renderFirstFactor(
res: express.Response) {
res.render("firstfactor", { res.render("firstfactor", {
first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST, first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST,
reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET reset_password_request_endpoint: Endpoints.RESET_PASSWORD_REQUEST_GET
}); });
} }
export default function (vars: ServerVariables) { export default function (
vars: ServerVariables) {
const redirector = new SafeRedirector(vars.config.session.domain);
return function (req: express.Request, res: express.Response): BluebirdPromise<void> { return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
return new BluebirdPromise(function (resolve, reject) { return new BluebirdPromise(function (resolve, reject) {
const authSession = AuthenticationSessionHandler.get(req, vars.logger); const authSession = AuthenticationSessionHandler.get(req, vars.logger);
if (authSession.first_factor) { if (authSession.first_factor) {
if (authSession.second_factor) if (authSession.second_factor)
redirectToService(req, res); redirectToService(req, res, redirector);
else else
redirectToSecondFactorPage(req, res); redirectToSecondFactorPage(req, res);
resolve(); resolve();

View File

@ -9,7 +9,7 @@ import Endpoint = require("../../../../../shared/api");
import ErrorReplies = require("../../ErrorReplies"); import ErrorReplies = require("../../ErrorReplies");
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler"; import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
import Constants = require("../../../../../shared/constants"); import Constants = require("../../../../../shared/constants");
import { DomainExtractor } from "../../utils/DomainExtractor"; import { DomainExtractor } from "../../../../../shared/DomainExtractor";
import UserMessages = require("../../../../../shared/UserMessages"); import UserMessages = require("../../../../../shared/UserMessages");
import { MethodCalculator } from "../../authentication/MethodCalculator"; import { MethodCalculator } from "../../authentication/MethodCalculator";
import { ServerVariables } from "../../ServerVariables"; import { ServerVariables } from "../../ServerVariables";
@ -51,14 +51,16 @@ export default function (vars: ServerVariables) {
authSession.userid = username; authSession.userid = username;
authSession.keep_me_logged_in = keepMeLoggedIn; authSession.keep_me_logged_in = keepMeLoggedIn;
authSession.first_factor = true; authSession.first_factor = true;
const redirectUrl = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined" const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined"
// Fuck, don't know why it is a string! // Fuck, don't know why it is a string!
? req.query[Constants.REDIRECT_QUERY_PARAM] ? req.query[Constants.REDIRECT_QUERY_PARAM]
: undefined; : undefined;
const emails: string[] = groupsAndEmails.emails; const emails: string[] = groupsAndEmails.emails;
const groups: string[] = groupsAndEmails.groups; const groups: string[] = groupsAndEmails.groups;
const redirectHost: string = DomainExtractor.fromUrl(redirectUrl);
const domain = DomainExtractor.fromUrl(redirectUrl);
const redirectHost = (domain) ? domain : "";
const authMethod = MethodCalculator.compute( const authMethod = MethodCalculator.compute(
vars.config.authentication_methods, redirectHost); vars.config.authentication_methods, redirectHost);
vars.logger.debug(req, "Authentication method for \"%s\" is \"%s\"", vars.logger.debug(req, "Authentication method for \"%s\" is \"%s\"",
@ -72,7 +74,7 @@ export default function (vars: ServerVariables) {
vars.regulator.mark(username, true); vars.regulator.mark(username, true);
if (authMethod == "single_factor") { if (authMethod == "single_factor") {
let newRedirectionUrl: string = redirectUrl; let newRedirectionUrl = redirectUrl;
if (!newRedirectionUrl) if (!newRedirectionUrl)
newRedirectionUrl = Endpoint.LOGGED_IN; newRedirectionUrl = Endpoint.LOGGED_IN;
res.send({ res.send({

View File

@ -24,7 +24,7 @@ export default function (vars: ServerVariables) {
}) })
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<void> { .then(function (doc: U2FRegistrationDocument): BluebirdPromise<void> {
if (!doc) if (!doc)
return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration found")); return BluebirdPromise.reject(new exceptions.AccessDeniedError("No U2F registration document found."));
const appId: string = u2f_common.extract_app_id(req); const appId: string = u2f_common.extract_app_id(req);
vars.logger.info(req, "Start authentication of app '%s'", appId); vars.logger.info(req, "Start authentication of app '%s'", appId);

View File

@ -4,7 +4,7 @@ import ObjectPath = require("object-path");
import { ServerVariables } from "../../ServerVariables"; import { ServerVariables } from "../../ServerVariables";
import { AuthenticationSession } import { AuthenticationSession }
from "../../../../types/AuthenticationSession"; from "../../../../types/AuthenticationSession";
import { DomainExtractor } from "../../utils/DomainExtractor"; import { DomainExtractor } from "../../../../../shared/DomainExtractor";
import { MethodCalculator } from "../../authentication/MethodCalculator"; import { MethodCalculator } from "../../authentication/MethodCalculator";
import AccessControl from "./access_control"; import AccessControl from "./access_control";

View File

@ -6,7 +6,7 @@ import ObjectPath = require("object-path");
import Exceptions = require("../../Exceptions"); import Exceptions = require("../../Exceptions");
import { Configuration } from "../../configuration/schema/Configuration"; import { Configuration } from "../../configuration/schema/Configuration";
import Constants = require("../../../../../shared/constants"); import Constants = require("../../../../../shared/constants");
import { DomainExtractor } from "../../utils/DomainExtractor"; import { DomainExtractor } from "../../../../../shared/DomainExtractor";
import { ServerVariables } from "../../ServerVariables"; import { ServerVariables } from "../../ServerVariables";
import { MethodCalculator } from "../../authentication/MethodCalculator"; import { MethodCalculator } from "../../authentication/MethodCalculator";
import { IRequestLogger } from "../../logging/IRequestLogger"; import { IRequestLogger } from "../../logging/IRequestLogger";

View File

@ -1,6 +0,0 @@
export class DomainExtractor {
static fromUrl(url: string): string {
if (!url) return "";
return url.match(/https?:\/\/([^\/:]+).*/)[1];
}
}

View File

@ -0,0 +1,33 @@
import Assert = require("assert");
import Sinon = require("sinon");
import { SafeRedirector } from "./SafeRedirection";
describe("web_server/middlewares/SafeRedirection", () => {
describe("Url is in protected domain", () => {
before(() => {
this.redirector = new SafeRedirector("example.com");
this.res = {redirect: Sinon.stub()};
});
it("should redirect to provided url", () => {
this.redirector.redirectOrElse(this.res,
"https://mysubdomain.example.com:8080/abc",
"https://authelia.example.com");
Assert(this.res.redirect.calledWith("https://mysubdomain.example.com:8080/abc"));
});
it("should redirect to default url when wrong domain", () => {
this.redirector.redirectOrElse(this.res,
"https://mysubdomain.domain.rtf:8080/abc",
"https://authelia.example.com");
Assert(this.res.redirect.calledWith("https://authelia.example.com"));
});
it("should redirect to default url when not terminating by domain", () => {
this.redirector.redirectOrElse(this.res,
"https://mysubdomain.example.com.rtf:8080/abc",
"https://authelia.example.com");
Assert(this.res.redirect.calledWith("https://authelia.example.com"));
});
});
});

View File

@ -0,0 +1,22 @@
import Express = require("express");
import { DomainExtractor } from "../../../../shared/DomainExtractor";
import { BelongToDomain } from "../../../../shared/BelongToDomain";
export class SafeRedirector {
private domain: string;
constructor(domain: string) {
this.domain = domain;
}
redirectOrElse(
res: Express.Response,
url: string,
defaultUrl: string): void {
if (BelongToDomain(url, this.domain)) {
res.redirect(url);
}
res.redirect(defaultUrl);
}
}

View File

@ -8,7 +8,7 @@ block form-header
block content block content
div div
div(class="notification notification-totp") div(class="notification")
h3 Hi <b>#{username}</b> h3 Hi <b>#{username}</b>
div(class="row") div(class="row")
div(class="u2f-token") div(class="u2f-token")

8
shared/BelongToDomain.ts Normal file
View File

@ -0,0 +1,8 @@
import { DomainExtractor } from "./DomainExtractor";
export function BelongToDomain(url: string, domain: string): boolean {
const urlDomain =  DomainExtractor.fromUrl(url);
if (!urlDomain) return false;
const idx = urlDomain.indexOf(domain);
return idx + domain.length == urlDomain.length;
}

View File

@ -1,7 +1,7 @@
import { DomainExtractor } from "./DomainExtractor"; import { DomainExtractor } from "./DomainExtractor";
import Assert = require("assert"); import Assert = require("assert");
describe("utils/DomainExtractor", function () { describe.only("shared/DomainExtractor", function () {
describe("test fromUrl", function () { describe("test fromUrl", function () {
it("should return domain from https url", function () { it("should return domain from https url", function () {
const domain = DomainExtractor.fromUrl("https://www.example.com/test/abc"); const domain = DomainExtractor.fromUrl("https://www.example.com/test/abc");
@ -17,5 +17,16 @@ describe("utils/DomainExtractor", function () {
const domain = DomainExtractor.fromUrl("https://www.example.com:8080/test/abc"); const domain = DomainExtractor.fromUrl("https://www.example.com:8080/test/abc");
Assert.equal(domain, "www.example.com"); Assert.equal(domain, "www.example.com");
}); });
it("should return domain when url contains redirect param", function () {
const domain0 = DomainExtractor.fromUrl("https://www.example.com:8080/test/abc?rd=https://cool.test.com");
Assert.equal(domain0, "www.example.com");
const domain1 = DomainExtractor.fromUrl("https://login.example.com:8080/?rd=https://public.example.com:8080/");
Assert.equal(domain1, "login.example.com");
const domain2 = DomainExtractor.fromUrl("https://single_factor.example.com:8080/secret.html");
Assert.equal(domain2, "single_factor.example.com");
});
}); });
}); });

11
shared/DomainExtractor.ts Normal file
View File

@ -0,0 +1,11 @@
export class DomainExtractor {
static fromUrl(url: string): string {
if (!url) return;
const matches = url.match(/(https?:\/\/)?([a-zA-Z0-9_.-]+).*/);
if (matches.length > 2) {
return matches[2];
}
return;
}
}

View File

@ -2,6 +2,8 @@
export const AUTHENTICATION_FAILED = "Authentication failed. Please check your credentials."; export const AUTHENTICATION_FAILED = "Authentication failed. Please check your credentials.";
export const AUTHENTICATION_SUCCEEDED = "Authentication succeeded. You can now access your services."; export const AUTHENTICATION_SUCCEEDED = "Authentication succeeded. You can now access your services.";
export const CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN = "Cannot redirect to an external domain.";
export const AUTHENTICATION_U2F_FAILED = "Authentication failed. Have you already registered your device?"; export const AUTHENTICATION_U2F_FAILED = "Authentication failed. Have you already registered your device?";
export const AUTHENTICATION_TOTP_FAILED = "Authentication failed. Have you already registered your secret?"; export const AUTHENTICATION_TOTP_FAILED = "Authentication failed. Have you already registered your secret?";

View File

@ -0,0 +1,41 @@
import WithDriver from "../helpers/with-driver";
import LoginAndRegisterTotp from "../helpers/login-and-register-totp";
import SeeNotification from "../helpers/see-notification";
import VisitPage from "../helpers/visit-page";
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import ValidateTotp from "../helpers/validate-totp";
import {CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN} from '../../shared/UserMessages';
/*
* Authelia should not be vulnerable to open redirection. Otherwise it would aid an
* attacker in conducting a phishing attack.
*
* To avoid the issue, Authelia's client scans the URL and prevent any redirection if
* the URL is pointing to an external domain.
*/
describe("Redirection should be performed only if in domain", function() {
this.timeout(10000);
WithDriver();
before(function() {
const that = this;
return LoginAndRegisterTotp(this.driver, "john", true)
.then((secret: string) => that.secret = secret)
});
function DoNotRedirect(url: string) {
it(`should see an error message instead of redirecting to ${url}`, function() {
const driver = this.driver;
const secret = this.secret;
return VisitPage(driver, `https://login.example.com:8080/?rd=${url}`)
.then(() => FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password'))
.then(() => ValidateTotp(driver, secret))
.then(() => SeeNotification(driver, "error", CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN))
.then(() => driver.get(`https://login.example.com:8080/logout`));
});
}
DoNotRedirect("www.google.fr");
DoNotRedirect("http://www.google.fr");
DoNotRedirect("https://www.google.fr");
})

View File

@ -1,19 +1,17 @@
require("chromedriver");
import SeleniumWebdriver = require("selenium-webdriver");
import WithDriver from '../helpers/with-driver'; import WithDriver from '../helpers/with-driver';
import LoginAndRegisterTotp from '../helpers/login-and-register-totp'; import fullLogin from '../helpers/full-login';
import LoginAs from '../helpers/login-as'; import loginAndRegisterTotp from '../helpers/login-and-register-totp';
import VisitPage from '../helpers/visit-page';
describe('Connection retry when mongo fails or restarts', function() { describe("Connection retry when mongo fails or restarts", function() {
this.timeout(20000); this.timeout(30000);
WithDriver(); WithDriver();
it('should be able to login after mongo restarts', function() { it("should be able to login after mongo restarts", function() {
const that = this; const that = this;
return that.environment.stop_service("mongo") let secret;
.then(() => that.environment.restart_service("authelia", 2000)) return loginAndRegisterTotp(that.driver, "john", true)
.then(() => that.environment.restart_service("mongo")) .then(_secret => secret = _secret)
.then(() => LoginAs(that.driver, "john")); .then(() => that.environment.restart_service("mongo", 1000))
.then(() => fullLogin(that.driver, "https://admin.example.com:8080/secret.html", "john", secret));
}) })
}); });

View File

@ -15,7 +15,7 @@ export class Environment {
private runCommand(command: string, timeout?: number): Bluebird<void> { private runCommand(command: string, timeout?: number): Bluebird<void> {
return new Bluebird<void>((resolve, reject) => { return new Bluebird<void>((resolve, reject) => {
console.log('[ENVIRONMENT] Running: %s', command); console.log('[ENVIRONMENT] Running: %s', command);
exec(command, (err, stdout, stderr) => { exec(command, (err: any, stdout: any, stderr: any) => {
if(err) { if(err) {
reject(err); reject(err);
return; return;

View File

@ -0,0 +1,12 @@
import VisitPage from "./visit-page";
import FillLoginPageWithUserAndPasswordAndClick from "./fill-login-page-and-click";
import ValidateTotp from "./validate-totp";
import WaitRedirected from "./wait-redirected";
// Validate the two factors!
export default function(driver: any, url: string, user: string, secret: string) {
return VisitPage(driver, `https://login.example.com:8080/?rd=${url}`)
.then(() => FillLoginPageWithUserAndPasswordAndClick(driver, user, 'password'))
.then(() => ValidateTotp(driver, secret))
.then(() => WaitRedirected(driver, "https://admin.example.com:8080/secret.html"));
}

View File

@ -1,10 +1,9 @@
import VisitPage from "./visit-page";
import FillLoginPageAndClick from './fill-login-page-and-click';
import RegisterTotp from './register-totp'; import RegisterTotp from './register-totp';
import WaitRedirected from './wait-redirected'; import WaitRedirected from './wait-redirected';
import LoginAs from './login-as'; import LoginAs from './login-as';
import Bluebird = require("bluebird");
export default function(driver: any, user: string, email?: boolean) { export default function(driver: any, user: string, email?: boolean): Bluebird<string> {
return LoginAs(driver, user) return LoginAs(driver, user)
.then(() => WaitRedirected(driver, "https://login.example.com:8080/secondfactor")) .then(() => WaitRedirected(driver, "https://login.example.com:8080/secondfactor"))
.then(() => RegisterTotp(driver, email)); .then(() => RegisterTotp(driver, email));

View File

@ -1,7 +1,5 @@
import VisitPage from "./visit-page"; import VisitPage from "./visit-page";
import FillLoginPageAndClick from './fill-login-page-and-click'; import FillLoginPageAndClick from './fill-login-page-and-click';
import RegisterTotp from './register-totp';
import WaitRedirected from './wait-redirected';
export default function(driver: any, user: string) { export default function(driver: any, user: string) {
return VisitPage(driver, "https://login.example.com:8080/") return VisitPage(driver, "https://login.example.com:8080/")

View File

@ -1,12 +1,8 @@
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
import WithDriver from '../helpers/with-driver'; import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click'; import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import WaitRedirected from '../helpers/wait-redirected';
import VisitPage from '../helpers/visit-page'; import VisitPage from '../helpers/visit-page';
import SeeNotification from '../helpers/see-notification'; import SeeNotification from '../helpers/see-notification';
import {AUTHENTICATION_FAILED} from '../../shared/UserMessages';
/** /**
* When user provides bad password, * When user provides bad password,
@ -28,7 +24,7 @@ describe("Provide bad password", function() {
it('should get a notification message', function() { it('should get a notification message', function() {
this.timeout(10000); this.timeout(10000);
return SeeNotification(this.driver, "error", "Authentication failed. Please check your credentials."); return SeeNotification(this.driver, "error", AUTHENTICATION_FAILED);
}); });
}); });
}); });

View File

@ -1,17 +1,11 @@
require("chromedriver"); require("chromedriver");
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
import WithDriver from '../helpers/with-driver'; import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click'; import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import WaitRedirected from '../helpers/wait-redirected';
import VisitPage from '../helpers/visit-page'; import VisitPage from '../helpers/visit-page';
import RegisterTotp from '../helpers/register-totp';
import ValidateTotp from '../helpers/validate-totp'; import ValidateTotp from '../helpers/validate-totp';
import AccessSecret from "../helpers/access-secret";
import LoginAndRegisterTotp from '../helpers/login-and-register-totp'; import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
import seeNotification from "../helpers/see-notification"; import seeNotification from "../helpers/see-notification";
import {AUTHENTICATION_TOTP_FAILED} from '../../shared/UserMessages';
/** /**
* Given john has registered a TOTP secret, * Given john has registered a TOTP secret,
@ -24,7 +18,6 @@ describe('Fail TOTP challenge', function() {
describe('successfully login as john', function() { describe('successfully login as john', function() {
before(function() { before(function() {
const that = this;
return LoginAndRegisterTotp(this.driver, "john", true); return LoginAndRegisterTotp(this.driver, "john", true);
}); });
@ -39,7 +32,7 @@ describe('Fail TOTP challenge', function() {
}); });
it("get a notification message", function() { it("get a notification message", function() {
return seeNotification(this.driver, "error", "Authentication failed. Have you already registered your secret?"); return seeNotification(this.driver, "error", AUTHENTICATION_TOTP_FAILED);
}); });
}); });
}); });

View File

@ -1,7 +1,6 @@
require("chromedriver"); require("chromedriver");
import Bluebird = require("bluebird"); import Bluebird = require("bluebird");
import ChildProcess = require("child_process"); import ChildProcess = require("child_process");
import SeleniumWebdriver = require("selenium-webdriver");
import WithDriver from '../helpers/with-driver'; import WithDriver from '../helpers/with-driver';
import VisitPage from '../helpers/visit-page'; import VisitPage from '../helpers/visit-page';

View File

@ -1,13 +1,9 @@
require("chromedriver"); require("chromedriver");
import Bluebird = require("bluebird"); import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import Fs = require("fs");
import Speakeasy = require("speakeasy");
import WithDriver from '../helpers/with-driver'; import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click'; import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import WaitRedirected from '../helpers/wait-redirected'; import WaitRedirected from '../helpers/wait-redirected';
import VisitPage from '../helpers/visit-page'; import VisitPage from '../helpers/visit-page';
import RegisterTotp from '../helpers/register-totp';
import ValidateTotp from '../helpers/validate-totp'; import ValidateTotp from '../helpers/validate-totp';
import AccessSecret from "../helpers/access-secret"; import AccessSecret from "../helpers/access-secret";
import LoginAndRegisterTotp from '../helpers/login-and-register-totp'; import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
@ -37,15 +33,9 @@ describe('Validate TOTP factor', function() {
const driver = this.driver; const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html") return VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html")
.then(() => { .then(() => FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password'))
return FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password'); .then(() => ValidateTotp(driver, secret))
}) .then(() => WaitRedirected(driver, "https://admin.example.com:8080/secret.html"));
.then(() => {
return ValidateTotp(driver, secret);
})
.then(() => {
return WaitRedirected(driver, "https://admin.example.com:8080/secret.html")
});
}); });
it("should access the secret", function() { it("should access the secret", function() {