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.pull/293/head
parent
5f8e33d6ac
commit
42581dfe93
10
Gruntfile.js
10
Gruntfile.js
|
@ -9,6 +9,9 @@ module.exports = function (grunt) {
|
|||
},
|
||||
"env-test-client-unit": {
|
||||
TS_NODE_PROJECT: "client/tsconfig.json"
|
||||
},
|
||||
"env-test-shared-unit": {
|
||||
TS_NODE_PROJECT: "server/tsconfig.json"
|
||||
}
|
||||
},
|
||||
run: {
|
||||
|
@ -37,6 +40,10 @@ module.exports = function (grunt) {
|
|||
cmd: "./node_modules/.bin/mocha",
|
||||
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": {
|
||||
cmd: "./node_modules/.bin/mocha",
|
||||
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('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-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('copy-resources', ['copy:resources', 'copy:views', 'copy:images', 'copy:thirdparties', 'concat:css']);
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -6,6 +6,7 @@ import { QueryParametersRetriever } from "../QueryParametersRetriever";
|
|||
import Constants = require("../../../../shared/constants");
|
||||
import Endpoints = require("../../../../shared/api");
|
||||
import UserMessages = require("../../../../shared/UserMessages");
|
||||
import { SafeRedirect } from "../SafeRedirect";
|
||||
|
||||
export default function (window: Window, $: JQueryStatic,
|
||||
firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) {
|
||||
|
@ -28,7 +29,9 @@ export default function (window: Window, $: JQueryStatic,
|
|||
}
|
||||
|
||||
function onFirstFactorSuccess(redirectUrl: string) {
|
||||
window.location.href = redirectUrl;
|
||||
SafeRedirect(redirectUrl, () => {
|
||||
notifier.error("Cannot redirect to an external domain.");
|
||||
});
|
||||
}
|
||||
|
||||
function onFirstFactorFailure(err: Error) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import U2f = require("u2f");
|
||||
import U2fApi from "u2f-api";
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import { SignMessage } from "../../../../shared/SignMessage";
|
||||
import Endpoints = require("../../../../shared/api");
|
||||
import UserMessages = require("../../../../shared/UserMessages");
|
||||
import { INotifier } from "../INotifier";
|
||||
|
@ -31,24 +30,13 @@ function finishU2fAuthentication(responseData: U2fApi.SignResponse,
|
|||
});
|
||||
}
|
||||
|
||||
function startU2fAuthentication($: JQueryStatic, notifier: INotifier)
|
||||
: BluebirdPromise<string> {
|
||||
|
||||
export function validate($: JQueryStatic): BluebirdPromise<string> {
|
||||
return GetPromised($, Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {},
|
||||
undefined, "json")
|
||||
.then(function (signRequest: U2f.Request) {
|
||||
notifier.info(UserMessages.PLEASE_TOUCH_TOKEN);
|
||||
return U2fApi.sign(signRequest, 60);
|
||||
})
|
||||
.then(function (signResponse: U2fApi.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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,38 +3,44 @@ import U2FValidator = require("./U2FValidator");
|
|||
import ClientConstants = require("./constants");
|
||||
import { Notifier } from "../Notifier";
|
||||
import { QueryParametersRetriever } from "../QueryParametersRetriever";
|
||||
import Endpoints = require("../../../../shared/api");
|
||||
import ServerConstants = require("../../../../shared/constants");
|
||||
import UserMessages = require("../../../../shared/UserMessages");
|
||||
import SharedConstants = require("../../../../shared/constants");
|
||||
import { SafeRedirect } from "../SafeRedirect";
|
||||
|
||||
export default function (window: Window, $: JQueryStatic) {
|
||||
const notifierTotp = new Notifier(".notification-totp", $);
|
||||
const notifierU2f = new Notifier(".notification-u2f", $);
|
||||
const notifier = new Notifier(".notification", $);
|
||||
|
||||
function onAuthenticationSuccess(serverRedirectUrl: string, notifier: Notifier) {
|
||||
if (QueryParametersRetriever.get(SharedConstants.REDIRECT_QUERY_PARAM))
|
||||
window.location.href = QueryParametersRetriever.get(SharedConstants.REDIRECT_QUERY_PARAM);
|
||||
else if (serverRedirectUrl)
|
||||
window.location.href = serverRedirectUrl;
|
||||
else
|
||||
function onAuthenticationSuccess(serverRedirectUrl: string) {
|
||||
const queryRedirectUrl = QueryParametersRetriever.get(SharedConstants.REDIRECT_QUERY_PARAM);
|
||||
if (queryRedirectUrl) {
|
||||
SafeRedirect(queryRedirectUrl, () => {
|
||||
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
|
||||
});
|
||||
} else if (serverRedirectUrl) {
|
||||
SafeRedirect(serverRedirectUrl, () => {
|
||||
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
|
||||
});
|
||||
} else {
|
||||
notifier.success(UserMessages.AUTHENTICATION_SUCCEEDED);
|
||||
}
|
||||
}
|
||||
|
||||
function onSecondFactorTotpSuccess(redirectUrl: string) {
|
||||
onAuthenticationSuccess(redirectUrl, notifierTotp);
|
||||
onAuthenticationSuccess(redirectUrl);
|
||||
}
|
||||
|
||||
function onSecondFactorTotpFailure(err: Error) {
|
||||
notifierTotp.error(UserMessages.AUTHENTICATION_TOTP_FAILED);
|
||||
notifier.error(UserMessages.AUTHENTICATION_TOTP_FAILED);
|
||||
}
|
||||
|
||||
function onU2fAuthenticationSuccess(redirectUrl: string) {
|
||||
onAuthenticationSuccess(redirectUrl, notifierU2f);
|
||||
onAuthenticationSuccess(redirectUrl);
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -47,7 +53,7 @@ export default function (window: Window, $: JQueryStatic) {
|
|||
|
||||
$(window.document).ready(function () {
|
||||
$(ClientConstants.TOTP_FORM_SELECTOR).on("submit", onTOTPFormSubmitted);
|
||||
U2FValidator.validate($, notifierU2f)
|
||||
U2FValidator.validate($)
|
||||
.then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure);
|
||||
});
|
||||
}
|
|
@ -8,6 +8,7 @@ import Endpoints = require("../../../../shared/api");
|
|||
import UserMessages = require("../../../../shared/UserMessages");
|
||||
import { RedirectionMessage } from "../../../../shared/RedirectionMessage";
|
||||
import { ErrorMessage } from "../../../../shared/ErrorMessage";
|
||||
import { SafeRedirect } from "../SafeRedirect";
|
||||
|
||||
export default function (window: Window, $: JQueryStatic) {
|
||||
const notifier = new Notifier(".notification", $);
|
||||
|
@ -44,7 +45,9 @@ export default function (window: Window, $: JQueryStatic) {
|
|||
$(document).ready(function () {
|
||||
requestRegistration()
|
||||
.then((redirectionUrl: string) => {
|
||||
document.location.href = redirectionUrl;
|
||||
SafeRedirect(redirectionUrl, () => {
|
||||
notifier.error(UserMessages.CANNOT_REDIRECT_TO_EXTERNAL_DOMAIN);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
onRegisterFailure(err);
|
||||
|
|
|
@ -7,45 +7,61 @@ import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler
|
|||
import Constants = require("../../../../../shared/constants");
|
||||
import Util = require("util");
|
||||
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"
|
||||
? req.query[Constants.REDIRECT_QUERY_PARAM]
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function redirectToSecondFactorPage(req: express.Request, res: express.Response) {
|
||||
function redirectToSecondFactorPage(
|
||||
req: express.Request,
|
||||
res: express.Response) {
|
||||
|
||||
const redirectUrl = getRedirectParam(req);
|
||||
if (!redirectUrl)
|
||||
res.redirect(Endpoints.SECOND_FACTOR_GET);
|
||||
else
|
||||
res.redirect(Util.format("%s?%s=%s", Endpoints.SECOND_FACTOR_GET,
|
||||
Constants.REDIRECT_QUERY_PARAM,
|
||||
redirectUrl));
|
||||
res.redirect(
|
||||
Util.format("%s?%s=%s",
|
||||
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);
|
||||
if (!redirectUrl)
|
||||
if (!redirectUrl) {
|
||||
res.redirect(Endpoints.LOGGED_IN);
|
||||
else
|
||||
res.redirect(redirectUrl);
|
||||
} else {
|
||||
redirector.redirectOrElse(res, redirectUrl, Endpoints.LOGGED_IN);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFirstFactor(res: express.Response) {
|
||||
function renderFirstFactor(
|
||||
res: express.Response) {
|
||||
|
||||
res.render("firstfactor", {
|
||||
first_factor_post_endpoint: Endpoints.FIRST_FACTOR_POST,
|
||||
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 new BluebirdPromise(function (resolve, reject) {
|
||||
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
if (authSession.first_factor) {
|
||||
if (authSession.second_factor)
|
||||
redirectToService(req, res);
|
||||
redirectToService(req, res, redirector);
|
||||
else
|
||||
redirectToSecondFactorPage(req, res);
|
||||
resolve();
|
||||
|
|
|
@ -9,7 +9,7 @@ import Endpoint = require("../../../../../shared/api");
|
|||
import ErrorReplies = require("../../ErrorReplies");
|
||||
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
|
||||
import Constants = require("../../../../../shared/constants");
|
||||
import { DomainExtractor } from "../../utils/DomainExtractor";
|
||||
import { DomainExtractor } from "../../../../../shared/DomainExtractor";
|
||||
import UserMessages = require("../../../../../shared/UserMessages");
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
|
@ -51,14 +51,16 @@ export default function (vars: ServerVariables) {
|
|||
authSession.userid = username;
|
||||
authSession.keep_me_logged_in = keepMeLoggedIn;
|
||||
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!
|
||||
? req.query[Constants.REDIRECT_QUERY_PARAM]
|
||||
: undefined;
|
||||
|
||||
const emails: string[] = groupsAndEmails.emails;
|
||||
const groups: string[] = groupsAndEmails.groups;
|
||||
const redirectHost: string = DomainExtractor.fromUrl(redirectUrl);
|
||||
|
||||
const domain = DomainExtractor.fromUrl(redirectUrl);
|
||||
const redirectHost = (domain) ? domain : "";
|
||||
const authMethod = MethodCalculator.compute(
|
||||
vars.config.authentication_methods, redirectHost);
|
||||
vars.logger.debug(req, "Authentication method for \"%s\" is \"%s\"",
|
||||
|
@ -72,7 +74,7 @@ export default function (vars: ServerVariables) {
|
|||
vars.regulator.mark(username, true);
|
||||
|
||||
if (authMethod == "single_factor") {
|
||||
let newRedirectionUrl: string = redirectUrl;
|
||||
let newRedirectionUrl = redirectUrl;
|
||||
if (!newRedirectionUrl)
|
||||
newRedirectionUrl = Endpoint.LOGGED_IN;
|
||||
res.send({
|
||||
|
|
|
@ -24,7 +24,7 @@ export default function (vars: ServerVariables) {
|
|||
})
|
||||
.then(function (doc: U2FRegistrationDocument): BluebirdPromise<void> {
|
||||
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);
|
||||
vars.logger.info(req, "Start authentication of app '%s'", appId);
|
||||
|
|
|
@ -4,7 +4,7 @@ import ObjectPath = require("object-path");
|
|||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { AuthenticationSession }
|
||||
from "../../../../types/AuthenticationSession";
|
||||
import { DomainExtractor } from "../../utils/DomainExtractor";
|
||||
import { DomainExtractor } from "../../../../../shared/DomainExtractor";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import AccessControl from "./access_control";
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import ObjectPath = require("object-path");
|
|||
import Exceptions = require("../../Exceptions");
|
||||
import { Configuration } from "../../configuration/schema/Configuration";
|
||||
import Constants = require("../../../../../shared/constants");
|
||||
import { DomainExtractor } from "../../utils/DomainExtractor";
|
||||
import { DomainExtractor } from "../../../../../shared/DomainExtractor";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import { IRequestLogger } from "../../logging/IRequestLogger";
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
export class DomainExtractor {
|
||||
static fromUrl(url: string): string {
|
||||
if (!url) return "";
|
||||
return url.match(/https?:\/\/([^\/:]+).*/)[1];
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ block form-header
|
|||
|
||||
block content
|
||||
div
|
||||
div(class="notification notification-totp")
|
||||
div(class="notification")
|
||||
h3 Hi <b>#{username}</b>
|
||||
div(class="row")
|
||||
div(class="u2f-token")
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { DomainExtractor } from "./DomainExtractor";
|
||||
import Assert = require("assert");
|
||||
|
||||
describe("utils/DomainExtractor", function () {
|
||||
describe.only("shared/DomainExtractor", function () {
|
||||
describe("test fromUrl", function () {
|
||||
it("should return domain from https url", function () {
|
||||
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");
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
export const AUTHENTICATION_FAILED = "Authentication failed. Please check your credentials.";
|
||||
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_TOTP_FAILED = "Authentication failed. Have you already registered your secret?";
|
||||
|
||||
|
|
|
@ -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");
|
||||
})
|
|
@ -1,19 +1,17 @@
|
|||
require("chromedriver");
|
||||
import SeleniumWebdriver = require("selenium-webdriver");
|
||||
import WithDriver from '../helpers/with-driver';
|
||||
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
|
||||
import LoginAs from '../helpers/login-as';
|
||||
import VisitPage from '../helpers/visit-page';
|
||||
import fullLogin from '../helpers/full-login';
|
||||
import loginAndRegisterTotp from '../helpers/login-and-register-totp';
|
||||
|
||||
describe('Connection retry when mongo fails or restarts', function() {
|
||||
this.timeout(20000);
|
||||
describe("Connection retry when mongo fails or restarts", function() {
|
||||
this.timeout(30000);
|
||||
WithDriver();
|
||||
|
||||
it('should be able to login after mongo restarts', function() {
|
||||
it("should be able to login after mongo restarts", function() {
|
||||
const that = this;
|
||||
return that.environment.stop_service("mongo")
|
||||
.then(() => that.environment.restart_service("authelia", 2000))
|
||||
.then(() => that.environment.restart_service("mongo"))
|
||||
.then(() => LoginAs(that.driver, "john"));
|
||||
let secret;
|
||||
return loginAndRegisterTotp(that.driver, "john", true)
|
||||
.then(_secret => secret = _secret)
|
||||
.then(() => that.environment.restart_service("mongo", 1000))
|
||||
.then(() => fullLogin(that.driver, "https://admin.example.com:8080/secret.html", "john", secret));
|
||||
})
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ export class Environment {
|
|||
private runCommand(command: string, timeout?: number): Bluebird<void> {
|
||||
return new Bluebird<void>((resolve, reject) => {
|
||||
console.log('[ENVIRONMENT] Running: %s', command);
|
||||
exec(command, (err, stdout, stderr) => {
|
||||
exec(command, (err: any, stdout: any, stderr: any) => {
|
||||
if(err) {
|
||||
reject(err);
|
||||
return;
|
||||
|
|
|
@ -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"));
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
import VisitPage from "./visit-page";
|
||||
import FillLoginPageAndClick from './fill-login-page-and-click';
|
||||
import RegisterTotp from './register-totp';
|
||||
import WaitRedirected from './wait-redirected';
|
||||
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)
|
||||
.then(() => WaitRedirected(driver, "https://login.example.com:8080/secondfactor"))
|
||||
.then(() => RegisterTotp(driver, email));
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import VisitPage from "./visit-page";
|
||||
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) {
|
||||
return VisitPage(driver, "https://login.example.com:8080/")
|
||||
|
|
|
@ -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 FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
|
||||
import WaitRedirected from '../helpers/wait-redirected';
|
||||
import VisitPage from '../helpers/visit-page';
|
||||
import SeeNotification from '../helpers/see-notification';
|
||||
import {AUTHENTICATION_FAILED} from '../../shared/UserMessages';
|
||||
|
||||
/**
|
||||
* When user provides bad password,
|
||||
|
@ -28,7 +24,7 @@ describe("Provide bad password", function() {
|
|||
|
||||
it('should get a notification message', function() {
|
||||
this.timeout(10000);
|
||||
return SeeNotification(this.driver, "error", "Authentication failed. Please check your credentials.");
|
||||
return SeeNotification(this.driver, "error", AUTHENTICATION_FAILED);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
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 FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
|
||||
import WaitRedirected from '../helpers/wait-redirected';
|
||||
import VisitPage from '../helpers/visit-page';
|
||||
import RegisterTotp from '../helpers/register-totp';
|
||||
import ValidateTotp from '../helpers/validate-totp';
|
||||
import AccessSecret from "../helpers/access-secret";
|
||||
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
|
||||
import seeNotification from "../helpers/see-notification";
|
||||
import {AUTHENTICATION_TOTP_FAILED} from '../../shared/UserMessages';
|
||||
|
||||
/**
|
||||
* Given john has registered a TOTP secret,
|
||||
|
@ -24,7 +18,6 @@ describe('Fail TOTP challenge', function() {
|
|||
|
||||
describe('successfully login as john', function() {
|
||||
before(function() {
|
||||
const that = this;
|
||||
return LoginAndRegisterTotp(this.driver, "john", true);
|
||||
});
|
||||
|
||||
|
@ -39,7 +32,7 @@ describe('Fail TOTP challenge', 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
require("chromedriver");
|
||||
import Bluebird = require("bluebird");
|
||||
import ChildProcess = require("child_process");
|
||||
import SeleniumWebdriver = require("selenium-webdriver");
|
||||
|
||||
import WithDriver from '../helpers/with-driver';
|
||||
import VisitPage from '../helpers/visit-page';
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
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 FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
|
||||
import WaitRedirected from '../helpers/wait-redirected';
|
||||
import VisitPage from '../helpers/visit-page';
|
||||
import RegisterTotp from '../helpers/register-totp';
|
||||
import ValidateTotp from '../helpers/validate-totp';
|
||||
import AccessSecret from "../helpers/access-secret";
|
||||
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
|
||||
|
@ -37,15 +33,9 @@ describe('Validate TOTP factor', function() {
|
|||
const driver = this.driver;
|
||||
|
||||
return VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html")
|
||||
.then(() => {
|
||||
return FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password');
|
||||
})
|
||||
.then(() => {
|
||||
return ValidateTotp(driver, secret);
|
||||
})
|
||||
.then(() => {
|
||||
return WaitRedirected(driver, "https://admin.example.com:8080/secret.html")
|
||||
});
|
||||
.then(() => FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password'))
|
||||
.then(() => ValidateTotp(driver, secret))
|
||||
.then(() => WaitRedirected(driver, "https://admin.example.com:8080/secret.html"));
|
||||
});
|
||||
|
||||
it("should access the secret", function() {
|
||||
|
|
Loading…
Reference in New Issue