Fix e2e tests for complete configuration.
parent
387187b152
commit
d2a547eca6
|
@ -0,0 +1,15 @@
|
|||
import { Dispatch } from "redux";
|
||||
import * as AutheliaService from '../services/AutheliaService';
|
||||
|
||||
export default async function(url: string, dispatch: Dispatch) {
|
||||
try {
|
||||
// Check the url against the backend before redirecting.
|
||||
await AutheliaService.checkRedirection(url);
|
||||
window.location.href = url;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Cannot redirect since the URL is not in the protected domain.' +
|
||||
'This behavior could be malicious so please the issue to an administrator.');
|
||||
throw e;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import * as AutheliaService from '../../../services/AutheliaService';
|
|||
import { push } from 'connected-react-router';
|
||||
import fetchState from '../../../behaviors/FetchStateBehavior';
|
||||
import LogoutBehavior from '../../../behaviors/LogoutBehavior';
|
||||
import SafelyRedirectBehavior from '../../../behaviors/SafelyRedirectBehavior';
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
securityKeySupported: state.secondFactor.securityKeySupported,
|
||||
|
@ -52,19 +53,23 @@ async function triggerSecurityKeySigning(dispatch: Dispatch) {
|
|||
await dispatch(securityKeySignSuccess());
|
||||
}
|
||||
|
||||
function redirectOnSuccess(dispatch: Dispatch, ownProps: OwnProps, duration?: number) {
|
||||
function redirect() {
|
||||
async function handleSuccess(dispatch: Dispatch, ownProps: OwnProps, duration?: number) {
|
||||
async function handle() {
|
||||
if (ownProps.redirection) {
|
||||
window.location.href = ownProps.redirection;
|
||||
try {
|
||||
await SafelyRedirectBehavior(ownProps.redirection, dispatch);
|
||||
} catch (e) {
|
||||
await fetchState(dispatch);
|
||||
}
|
||||
} else {
|
||||
fetchState(dispatch);
|
||||
await fetchState(dispatch);
|
||||
}
|
||||
}
|
||||
|
||||
if (duration) {
|
||||
setTimeout(redirect, duration);
|
||||
setTimeout(handle, duration);
|
||||
} else {
|
||||
redirect();
|
||||
await handle();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +89,7 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
|
|||
if (isU2FSupported) {
|
||||
await dispatch(setSecurityKeySupported(true));
|
||||
await triggerSecurityKeySigning(dispatch);
|
||||
redirectOnSuccess(dispatch, ownProps, 1000);
|
||||
await handleSuccess(dispatch, ownProps, 1000);
|
||||
}
|
||||
},
|
||||
onOneTimePasswordValidationRequested: async (token: string) => {
|
||||
|
@ -106,7 +111,7 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
|
|||
throw body['error'];
|
||||
}
|
||||
dispatch(oneTimePasswordVerificationSuccess());
|
||||
redirectOnSuccess(dispatch, ownProps);
|
||||
await handleSuccess(dispatch, ownProps);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import AuthenticationView, {StateProps, Stage, DispatchProps} from '../../../views/AuthenticationView/AuthenticationView';
|
||||
import QueryString from 'query-string';
|
||||
import AuthenticationView, {StateProps, Stage, DispatchProps, OwnProps} from '../../../views/AuthenticationView/AuthenticationView';
|
||||
import { RootState } from '../../../reducers';
|
||||
import { Dispatch } from 'redux';
|
||||
import AuthenticationLevel from '../../../types/AuthenticationLevel';
|
||||
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
|
||||
import { setRedirectionUrl } from '../../../reducers/Portal/Authentication/actions';
|
||||
|
||||
function authenticationLevelToStage(level: AuthenticationLevel): Stage {
|
||||
switch (level) {
|
||||
|
@ -17,12 +17,21 @@ function authenticationLevelToStage(level: AuthenticationLevel): Stage {
|
|||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => {
|
||||
const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
|
||||
const stage = (state.authentication.remoteState)
|
||||
? authenticationLevelToStage(state.authentication.remoteState.authentication_level)
|
||||
: Stage.FIRST_FACTOR;
|
||||
|
||||
let url: string | null = null;
|
||||
if (ownProps.location) {
|
||||
const params = QueryString.parse(ownProps.location.search);
|
||||
if ('rd' in params) {
|
||||
url = params['rd'] as string;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
redirectionUrl: state.authentication.redirectionUrl,
|
||||
redirectionUrl: url,
|
||||
remoteState: state.authentication.remoteState,
|
||||
stage: stage,
|
||||
};
|
||||
|
@ -30,11 +39,8 @@ const mapStateToProps = (state: RootState): StateProps => {
|
|||
|
||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
|
||||
return {
|
||||
onInit: async (redirectionUrl?: string) => {
|
||||
onInit: async () => {
|
||||
await FetchStateBehavior(dispatch);
|
||||
if (redirectionUrl) {
|
||||
await dispatch(setRedirectionUrl(redirectionUrl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,4 +114,24 @@ export async function resetPassword(newPassword: string) {
|
|||
},
|
||||
body: JSON.stringify({password: newPassword})
|
||||
});
|
||||
}
|
||||
|
||||
export async function checkRedirection(url: string) {
|
||||
const res = await fetch('/api/redirect', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({url})
|
||||
})
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Status code ' + res.status);
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
if (text !== 'OK') {
|
||||
throw new Error('Cannot redirect');
|
||||
}
|
||||
return;
|
||||
}
|
|
@ -3,8 +3,7 @@ import AlreadyAuthenticated from "../../containers/components/AlreadyAuthenticat
|
|||
import FirstFactorForm from "../../containers/components/FirstFactorForm/FirstFactorForm";
|
||||
import SecondFactorForm from "../../containers/components/SecondFactorForm/SecondFactorForm";
|
||||
import RemoteState from "./RemoteState";
|
||||
import { RouterProps } from "react-router";
|
||||
import queryString from 'query-string';
|
||||
import { RouterProps, RouteProps } from "react-router";
|
||||
|
||||
export enum Stage {
|
||||
FIRST_FACTOR,
|
||||
|
@ -12,6 +11,8 @@ export enum Stage {
|
|||
ALREADY_AUTHENTICATED,
|
||||
}
|
||||
|
||||
export interface OwnProps extends RouteProps {}
|
||||
|
||||
export interface StateProps {
|
||||
stage: Stage;
|
||||
remoteState: RemoteState | null;
|
||||
|
@ -19,19 +20,13 @@ export interface StateProps {
|
|||
}
|
||||
|
||||
export interface DispatchProps {
|
||||
onInit: (redirectionUrl?: string) => void;
|
||||
onInit: () => void;
|
||||
}
|
||||
|
||||
export type Props = StateProps & DispatchProps & RouterProps;
|
||||
|
||||
class AuthenticationView extends Component<Props> {
|
||||
componentDidMount() {
|
||||
if (this.props.history.location) {
|
||||
const params = queryString.parse(this.props.history.location.search);
|
||||
if ('rd' in params) {
|
||||
this.props.onInit(params['rd'] as string);
|
||||
}
|
||||
}
|
||||
componentWillMount() {
|
||||
this.props.onInit();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
port: 9091
|
||||
|
||||
log_level: debug
|
||||
|
||||
authentication_backend:
|
||||
file:
|
||||
path: ./users_database.yml
|
||||
|
|
|
@ -532,6 +532,11 @@
|
|||
"integrity": "sha512-vOVmaruQG5EatOU/jM6yU2uCp3Lz6mK1P5Ztu4iJjfM4SVHU9XYktPUQtKlIXuahqXHdEyUarMrBEwg5Cwu+bA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/url-parse": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/url-parse/-/url-parse-1.4.2.tgz",
|
||||
"integrity": "sha512-Z25Ef2iI0lkiBAoe8qATRHQWy+BWFWuWasD6HB5prsWx2QjLmB/ngXv8v9dePw2jDwdSvET4/6J5HxmeqhslmQ=="
|
||||
},
|
||||
"@types/winston": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.3.9.tgz",
|
||||
|
@ -7053,6 +7058,11 @@
|
|||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
|
||||
"dev": true
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.0.tgz",
|
||||
"integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg=="
|
||||
},
|
||||
"random-bytes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||
|
@ -7424,6 +7434,11 @@
|
|||
"semver": "5.5.0"
|
||||
}
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz",
|
||||
|
@ -8924,6 +8939,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.4.tgz",
|
||||
"integrity": "sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==",
|
||||
"requires": {
|
||||
"querystringify": "2.1.0",
|
||||
"requires-port": "1.0.0"
|
||||
}
|
||||
},
|
||||
"url-parse-lax": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"title": "Authelia API documentation"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/url-parse": "^1.4.2",
|
||||
"ajv": "^6.3.0",
|
||||
"bluebird": "^3.5.0",
|
||||
"body-parser": "^1.15.2",
|
||||
|
@ -50,6 +51,7 @@
|
|||
"speakeasy": "^2.0.0",
|
||||
"u2f": "^0.1.2",
|
||||
"u2f-api": "^1.0.7",
|
||||
"url-parse": "^1.4.4",
|
||||
"winston": "^2.3.1",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import * as Express from "express";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
|
||||
import { Level } from "../../authentication/Level";
|
||||
import * as URLParse from "url-parse";
|
||||
|
||||
export default function (vars: ServerVariables) {
|
||||
return function (req: Express.Request, res: Express.Response) {
|
||||
if (!req.body.url) {
|
||||
res.status(400);
|
||||
vars.logger.error(req, "Provide url for verification to be done.");
|
||||
return;
|
||||
}
|
||||
|
||||
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
const url = new URLParse(req.body.url);
|
||||
|
||||
const urlInDomain = url.hostname.endsWith(vars.config.session.domain);
|
||||
vars.logger.debug(req, "Check domain %s is in url %s.", vars.config.session.domain, url.hostname);
|
||||
const sufficientPermissions = authSession.authentication_level >= Level.TWO_FACTOR;
|
||||
|
||||
vars.logger.debug(req, "Check that protocol %s is HTTPS.", url.protocol);
|
||||
const protocolIsHttps = url.protocol === "https:";
|
||||
|
||||
if (sufficientPermissions && urlInDomain && protocolIsHttps) {
|
||||
res.send("OK");
|
||||
return;
|
||||
}
|
||||
res.send("NOK");
|
||||
};
|
||||
}
|
|
@ -3,6 +3,7 @@ import Express = require("express");
|
|||
import FirstFactorPost = require("../routes/firstfactor/post");
|
||||
import LogoutPost from "../routes/logout/post";
|
||||
import StateGet from "../routes/state/get";
|
||||
import RedirectPost from "../routes/redirect/post";
|
||||
import VerifyGet = require("../routes/verify/get");
|
||||
import TOTPSignGet = require("../routes/secondfactor/totp/sign/post");
|
||||
|
||||
|
@ -86,6 +87,7 @@ function setupResetPassword(app: Express.Application, vars: ServerVariables) {
|
|||
export class RestApi {
|
||||
static setup(app: Express.Application, vars: ServerVariables): void {
|
||||
app.get(Endpoints.STATE_GET, StateGet(vars));
|
||||
app.post(Endpoints.REDIRECT_POST, RedirectPost(vars));
|
||||
|
||||
app.post(Endpoints.LOGOUT_POST, LogoutPost(vars));
|
||||
|
||||
|
|
|
@ -287,3 +287,17 @@ export const VERIFY_GET = "/api/verify";
|
|||
*/
|
||||
export const LOGOUT_POST = "/api/logout";
|
||||
|
||||
/**
|
||||
* @api {post} /api/redirect Url redirection checking endpoint
|
||||
* @apiName Redirect
|
||||
* @apiGroup Authentication
|
||||
* @apiVersion 1.0.0
|
||||
* @apiDescription Check if the user can be redirected to the url provided.
|
||||
* The level of permissions for this user are checked and the url must be
|
||||
* in the domain protected by authelia.
|
||||
*
|
||||
* @apiSuccess (Success 200)
|
||||
*
|
||||
* @apiDescription Resets the session to logout the user.
|
||||
*/
|
||||
export const REDIRECT_POST = "/api/redirect";
|
|
@ -1,28 +0,0 @@
|
|||
require("chromedriver");
|
||||
import Environment = require('../environment');
|
||||
|
||||
const includes = [
|
||||
"docker-compose.yml",
|
||||
"example/compose/docker-compose.base.yml",
|
||||
"example/compose/mongo/docker-compose.yml",
|
||||
"example/compose/redis/docker-compose.yml",
|
||||
"example/compose/nginx/backend/docker-compose.yml",
|
||||
"example/compose/nginx/portal/docker-compose.yml",
|
||||
"example/compose/smtp/docker-compose.yml",
|
||||
"example/compose/httpbin/docker-compose.yml",
|
||||
"example/compose/ldap/docker-compose.yml"
|
||||
];
|
||||
|
||||
|
||||
before(function() {
|
||||
this.timeout(20000);
|
||||
this.environment = new Environment.Environment(includes);
|
||||
return this.environment.setup(5000);
|
||||
});
|
||||
|
||||
after(function() {
|
||||
this.timeout(30000);
|
||||
if(process.env.KEEP_ENV != "true") {
|
||||
return this.environment.cleanup();
|
||||
}
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
import WithDriver from "../helpers/context/WithDriver";
|
||||
import LoginAndRegisterTotp from "../helpers/LoginAndRegisterTotp";
|
||||
import SeeNotification from "../helpers/SeeNotification";
|
||||
import VisitPage from "../helpers/VisitPage";
|
||||
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/FillLoginPageAndClick';
|
||||
import ValidateTotp from "../helpers/ValidateTotp";
|
||||
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,17 +0,0 @@
|
|||
import WithDriver from '../helpers/context/WithDriver';
|
||||
import fullLogin from '../helpers/FullLogin';
|
||||
import loginAndRegisterTotp from '../helpers/LoginAndRegisterTotp';
|
||||
|
||||
describe("Connection retry when mongo fails or restarts", function() {
|
||||
this.timeout(30000);
|
||||
WithDriver();
|
||||
|
||||
it("should be able to login after mongo restarts", function() {
|
||||
const that = this;
|
||||
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));
|
||||
})
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import SeleniumWebDriver, { WebDriver } from "selenium-webdriver";
|
||||
|
||||
export default async function(driver: WebDriver) {
|
||||
await driver.wait(SeleniumWebDriver.until.elementLocated(SeleniumWebDriver.By.className('already-authenticated-step')));
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { WebDriver } from "selenium-webdriver";
|
||||
|
||||
export default async function(driver: WebDriver) {
|
||||
await driver.get(`https://login.example.com:8080/logout`);
|
||||
}
|
|
@ -4,15 +4,15 @@ import WithDriver from "./WithDriver";
|
|||
let running = false;
|
||||
|
||||
interface AutheliaSuiteType {
|
||||
(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void): Mocha.ISuite;
|
||||
only: (description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite;
|
||||
(description: string, configPath: string, cb: (this: Mocha.ISuiteCallbackContext) => void): Mocha.ISuite;
|
||||
only: (description: string, configPath: string, cb: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite;
|
||||
}
|
||||
|
||||
function AutheliaSuiteBase(description: string,
|
||||
context: (description: string, ctx: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite,
|
||||
cb: (this: Mocha.ISuiteCallbackContext) => void) {
|
||||
function AutheliaSuiteBase(description: string, configPath: string,
|
||||
cb: (this: Mocha.ISuiteCallbackContext) => void,
|
||||
context: (description: string, ctx: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite) {
|
||||
if (!running && process.env['WITH_SERVER'] == 'y') {
|
||||
WithAutheliaRunning();
|
||||
WithAutheliaRunning(configPath);
|
||||
running = true;
|
||||
}
|
||||
|
||||
|
@ -22,13 +22,16 @@ function AutheliaSuiteBase(description: string,
|
|||
});
|
||||
}
|
||||
|
||||
const AutheliaSuite = <AutheliaSuiteType>function(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) {
|
||||
return AutheliaSuiteBase(description, describe, cb);
|
||||
const AutheliaSuite = <AutheliaSuiteType>function(
|
||||
description: string, configPath: string,
|
||||
cb: (this: Mocha.ISuiteCallbackContext) => void) {
|
||||
return AutheliaSuiteBase(description, configPath, cb, describe);
|
||||
}
|
||||
|
||||
|
||||
AutheliaSuite.only = function(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) {
|
||||
return AutheliaSuiteBase(description, describe.only, cb);
|
||||
AutheliaSuite.only = function(description: string, configPath: string,
|
||||
cb: (this: Mocha.ISuiteCallbackContext) => void) {
|
||||
return AutheliaSuiteBase(description, configPath, cb, describe.only);
|
||||
}
|
||||
|
||||
export default AutheliaSuite as AutheliaSuiteType;
|
|
@ -1,12 +1,12 @@
|
|||
|
||||
import ChildProcess from 'child_process';
|
||||
|
||||
export default function WithAutheliaRunning(waitTimeout: number = 3000) {
|
||||
export default function WithAutheliaRunning(configPath: string, waitTimeout: number = 3000) {
|
||||
before(function() {
|
||||
this.timeout(5000);
|
||||
const authelia = ChildProcess.spawn(
|
||||
'./scripts/authelia-scripts',
|
||||
['serve', '--no-watch', '--config', 'config.minimal.yml'],
|
||||
['serve', '--no-watch', '--config', configPath],
|
||||
{detached: true});
|
||||
this.authelia = authelia;
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ export default function() {
|
|||
beforeEach(function() {
|
||||
const driver = new SeleniumWebdriver.Builder()
|
||||
.forBrowser("chrome")
|
||||
.setChromeOptions(
|
||||
new chrome.Options().headless())
|
||||
.setChromeOptions(new chrome.Options().headless())
|
||||
.build();
|
||||
this.driver = driver;
|
||||
});
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import AutheliaSuite from "../../helpers/context/AutheliaSuite";
|
||||
import MongoConnectionRecovery from "./scenarii/MongoConnectionRecovery";
|
||||
import EnforceInternalRedirectionsOnly from "./scenarii/EnforceInternalRedirectionsOnly";
|
||||
|
||||
AutheliaSuite('Complete configuration', 'config.template.yml', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
describe('Mongo broken connection recovery', MongoConnectionRecovery);
|
||||
describe('Enforce internal redirections only', EnforceInternalRedirectionsOnly);
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp";
|
||||
import VisitPage from "../../../helpers/VisitPage";
|
||||
import FillLoginPageWithUserAndPasswordAndClick from '../../../helpers/FillLoginPageAndClick';
|
||||
import ValidateTotp from "../../../helpers/ValidateTotp";
|
||||
import Logout from "../../../helpers/Logout";
|
||||
import WaitRedirected from "../../../helpers/WaitRedirected";
|
||||
import IsAlreadyAuthenticatedStage from "../../../helpers/IsAlreadyAuthenticatedStage";
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
export default function() {
|
||||
describe("Only redirection to a subdomain of the protected domain should be allowed", function() {
|
||||
this.timeout(10000);
|
||||
let secret: string;
|
||||
|
||||
beforeEach(async function() {
|
||||
secret = await LoginAndRegisterTotp(this.driver, "john", true)
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
await Logout(this.driver);
|
||||
})
|
||||
|
||||
function CannotRedirectTo(url: string) {
|
||||
it(`should redirect to already authenticated page when requesting ${url}`, async function() {
|
||||
await VisitPage(this.driver, `https://login.example.com:8080/?rd=${url}`);
|
||||
await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'password');
|
||||
await ValidateTotp(this.driver, secret);
|
||||
await IsAlreadyAuthenticatedStage(this.driver);
|
||||
});
|
||||
}
|
||||
|
||||
function CanRedirectTo(url: string) {
|
||||
it(`should redirect to ${url}`, async function() {
|
||||
await VisitPage(this.driver, `https://login.example.com:8080/?rd=${url}`);
|
||||
await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'password');
|
||||
await ValidateTotp(this.driver, secret);
|
||||
await WaitRedirected(this.driver, url);
|
||||
});
|
||||
}
|
||||
|
||||
describe('blocked redirection', function() {
|
||||
// Do not redirect to another domain than example.com
|
||||
CannotRedirectTo("https://www.google.fr");
|
||||
|
||||
// Do not redirect to rogue domain
|
||||
CannotRedirectTo("https://public.example.com.a:8080");
|
||||
|
||||
// Do not redirect to http website
|
||||
CannotRedirectTo("http://public.example.com:8080");
|
||||
});
|
||||
|
||||
describe('allowed redirection', function() {
|
||||
// Can redirect to any subdomain of the domain protected by Authelia.
|
||||
CanRedirectTo("https://public.example.com:8080/");
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp";
|
||||
import FullLogin from "../../../helpers/FullLogin";
|
||||
import child_process from 'child_process';
|
||||
|
||||
export default function() {
|
||||
it("should be able to login after mongo restarts", async function() {
|
||||
this.timeout(30000);
|
||||
const secret = await LoginAndRegisterTotp(this.driver, "john", true);
|
||||
child_process.execSync("./scripts/dc-dev.sh restart mongo");
|
||||
await FullLogin(this.driver, "https://admin.example.com:8080/secret.html", "john", secret);
|
||||
});
|
||||
}
|
|
@ -9,7 +9,7 @@ import TOTPValidation from './scenarii/TOTPValidation';
|
|||
|
||||
const execAsync = Bluebird.promisify(ChildProcess.exec);
|
||||
|
||||
AutheliaSuite('Minimal configuration', function() {
|
||||
AutheliaSuite('Minimal configuration', 'config.minimal.yml', function() {
|
||||
this.timeout(10000);
|
||||
beforeEach(function() {
|
||||
return execAsync("cp users_database.example.yml users_database.yml");
|
||||
|
|
Loading…
Reference in New Issue