Fix open redirection vulnerability.

In order to redirect the user after authentication, Authelia uses
rd query parameter provided by the proxy. However an attacker could
use phishing to make the user be redirected to a bad domain. In order
to avoid the user to be redirected to a bad location, Authelia now
verifies the redirection URL is under the protected domain.
This commit is contained in:
Clement Michaud 2018-10-27 18:18:25 +02:00 committed by Clement Michaud
parent 5f8e33d6ac
commit 42581dfe93
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() {