Merge pull request #293 from clems4ever/closed-redirection
Fix open redirection vulnerability.pull/308/head
commit
d898fa2c0c
10
Gruntfile.js
10
Gruntfile.js
|
@ -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']);
|
||||||
|
|
|
@ -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 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) {
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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
|
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")
|
||||||
|
|
|
@ -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 { 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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -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_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?";
|
||||||
|
|
||||||
|
|
|
@ -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 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));
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 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));
|
||||||
|
|
|
@ -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/")
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in New Issue