From c579355c5b99d40d85dd0b3be07986986f42cbb5 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Tue, 12 Feb 2019 23:23:43 +0100 Subject: [PATCH] Migrate more Cucumber tests into Mocha. --- .gitignore | 2 + client/src/.env.development | 0 .../src/behaviors/SafelyRedirectBehavior.ts | 2 +- .../FirstFactorForm/FirstFactorForm.tsx | 6 ++- .../SecondFactorForm/SecondFactorForm.tsx | 2 +- .../FirstFactorForm/FirstFactorForm.ts | 31 +++++++---- .../SecondFactorForm/SecondFactorForm.ts | 4 +- .../AuthenticationView/AuthenticationView.ts | 6 +-- client/src/reducers/index.ts | 1 + client/src/services/AutheliaService.ts | 17 +++++-- .../AuthenticationView/AuthenticationView.tsx | 4 +- scripts/authelia-scripts-start | 43 ++++++++++++---- server/src/lib/authorization/Authorizer.ts | 2 +- server/src/lib/routes/firstfactor/post.ts | 37 ++++++++++++++ .../lib/routes/verify/get_session_cookie.ts | 2 - server/src/lib/utils/SafeRedirection.ts | 1 - server/src/lib/utils/URLDecomposer.ts | 1 + test/features/forward-headers.feature | 11 ---- test/helpers/AccessSecret.ts | 14 ----- test/helpers/ClickOn.ts | 1 - test/helpers/FullLogin.ts | 10 ++-- test/helpers/LoginAndRegisterTotp.ts | 6 +-- test/helpers/LoginAs.ts | 4 +- .../assertions/VerifyForwardedHeaderIs.ts | 13 +++++ .../VerifyIsAlreadyAuthenticatedStage.ts} | 0 .../VerifyIsSecondFactorStage.ts} | 0 ...serveSecret.ts => VerifySecretObserved.ts} | 0 test/helpers/assertions/VerifyUrlIs.ts | 5 ++ test/helpers/behaviors/LoginOneFactor.ts | 17 +++++++ .../behaviors/RegisterAndLoginTwoFactor.ts | 13 +++++ test/helpers/context/WithDriver.ts | 40 +++++++++------ test/suites/complete/index.ts | 2 + .../suites/complete/scenarii/AccessControl.ts | 8 +-- .../scenarii/CustomHeadersForwarded.ts | 51 +++++++++++++++++++ .../EnforceInternalRedirectionsOnly.ts | 7 +-- .../scenarii/MongoConnectionRecovery.ts | 2 +- test/suites/minimal/scenarii/Inactivity.ts | 1 - test/suites/minimal/scenarii/ResetPassword.ts | 2 +- .../suites/minimal/scenarii/TOTPValidation.ts | 4 +- 39 files changed, 267 insertions(+), 105 deletions(-) delete mode 100755 client/src/.env.development delete mode 100644 test/features/forward-headers.feature delete mode 100644 test/helpers/AccessSecret.ts create mode 100644 test/helpers/assertions/VerifyForwardedHeaderIs.ts rename test/helpers/{IsAlreadyAuthenticatedStage.ts => assertions/VerifyIsAlreadyAuthenticatedStage.ts} (100%) rename test/helpers/{IsSecondFactorStage.ts => assertions/VerifyIsSecondFactorStage.ts} (100%) rename test/helpers/assertions/{ObserveSecret.ts => VerifySecretObserved.ts} (100%) create mode 100644 test/helpers/assertions/VerifyUrlIs.ts create mode 100644 test/helpers/behaviors/LoginOneFactor.ts create mode 100644 test/helpers/behaviors/RegisterAndLoginTwoFactor.ts create mode 100644 test/suites/complete/scenarii/CustomHeadersForwarded.ts diff --git a/.gitignore b/.gitignore index eb1f1753..166cceaa 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ example/ldap/private.ldif Configuration.schema.json users_database.test.yml + +.suite diff --git a/client/src/.env.development b/client/src/.env.development deleted file mode 100755 index e69de29b..00000000 diff --git a/client/src/behaviors/SafelyRedirectBehavior.ts b/client/src/behaviors/SafelyRedirectBehavior.ts index 8f73d6c0..0d683456 100644 --- a/client/src/behaviors/SafelyRedirectBehavior.ts +++ b/client/src/behaviors/SafelyRedirectBehavior.ts @@ -1,7 +1,7 @@ import { Dispatch } from "redux"; import * as AutheliaService from '../services/AutheliaService'; -export default async function(url: string, dispatch: Dispatch) { +export default async function(url: string) { try { // Check the url against the backend before redirecting. await AutheliaService.checkRedirection(url); diff --git a/client/src/components/FirstFactorForm/FirstFactorForm.tsx b/client/src/components/FirstFactorForm/FirstFactorForm.tsx index ec702213..57a1d323 100644 --- a/client/src/components/FirstFactorForm/FirstFactorForm.tsx +++ b/client/src/components/FirstFactorForm/FirstFactorForm.tsx @@ -10,6 +10,10 @@ import Notification from "../../components/Notification/Notification"; import styles from '../../assets/scss/components/FirstFactorForm/FirstFactorForm.module.scss'; +export interface OwnProps { + redirectionUrl: string | null; +} + export interface StateProps { formDisabled: boolean; error: string | null; @@ -19,7 +23,7 @@ export interface DispatchProps { onAuthenticationRequested(username: string, password: string, rememberMe: boolean): void; } -export type Props = StateProps & DispatchProps; +export type Props = OwnProps & StateProps & DispatchProps; interface State { username: string; diff --git a/client/src/components/SecondFactorForm/SecondFactorForm.tsx b/client/src/components/SecondFactorForm/SecondFactorForm.tsx index 2af9585a..a2ac3b3e 100644 --- a/client/src/components/SecondFactorForm/SecondFactorForm.tsx +++ b/client/src/components/SecondFactorForm/SecondFactorForm.tsx @@ -10,7 +10,7 @@ import Notification from '../Notification/Notification'; export interface OwnProps { username: string; - redirection: string | null; + redirectionUrl: string | null; } export interface StateProps { diff --git a/client/src/containers/components/FirstFactorForm/FirstFactorForm.ts b/client/src/containers/components/FirstFactorForm/FirstFactorForm.ts index 8ea64a54..e47ad617 100644 --- a/client/src/containers/components/FirstFactorForm/FirstFactorForm.ts +++ b/client/src/containers/components/FirstFactorForm/FirstFactorForm.ts @@ -1,11 +1,12 @@ import { connect } from 'react-redux'; import { Dispatch } from 'redux'; import { authenticateFailure, authenticateSuccess, authenticate } from '../../../reducers/Portal/FirstFactor/actions'; -import FirstFactorForm, { StateProps } from '../../../components/FirstFactorForm/FirstFactorForm'; +import FirstFactorForm, { StateProps, OwnProps } from '../../../components/FirstFactorForm/FirstFactorForm'; import { RootState } from '../../../reducers'; import * as AutheliaService from '../../../services/AutheliaService'; import to from 'await-to-js'; import FetchStateBehavior from '../../../behaviors/FetchStateBehavior'; +import SafelyRedirectBehavior from '../../../behaviors/SafelyRedirectBehavior'; const mapStateToProps = (state: RootState): StateProps => { return { @@ -14,13 +15,14 @@ const mapStateToProps = (state: RootState): StateProps => { }; } -function onAuthenticationRequested(dispatch: Dispatch) { +function onAuthenticationRequested(dispatch: Dispatch, redirectionUrl: string | null) { return async (username: string, password: string, rememberMe: boolean) => { let err, res; // Validate first factor dispatch(authenticate()); - [err, res] = await to(AutheliaService.postFirstFactorAuth(username, password, rememberMe)); + [err, res] = await to(AutheliaService.postFirstFactorAuth( + username, password, rememberMe, redirectionUrl)); if (err) { await dispatch(authenticateFailure(err.message)); @@ -32,24 +34,31 @@ function onAuthenticationRequested(dispatch: Dispatch) { return; } - if (res.status !== 204) { + if (res.status === 200) { const json = await res.json(); if ('error' in json) { await dispatch(authenticateFailure(json['error'])); return; } - } - - dispatch(authenticateSuccess()); - // fetch state - FetchStateBehavior(dispatch); + if ('redirect' in json) { + window.location.href = json['redirect']; + return; + } + } else if (res.status === 204) { + dispatch(authenticateSuccess()); + + // fetch state to move to next stage + FetchStateBehavior(dispatch); + } else { + dispatch(authenticateFailure('Unknown error')); + } } } -const mapDispatchToProps = (dispatch: Dispatch) => { +const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => { return { - onAuthenticationRequested: onAuthenticationRequested(dispatch), + onAuthenticationRequested: onAuthenticationRequested(dispatch, ownProps.redirectionUrl), } } diff --git a/client/src/containers/components/SecondFactorForm/SecondFactorForm.ts b/client/src/containers/components/SecondFactorForm/SecondFactorForm.ts index 41f83fa2..804abdc9 100644 --- a/client/src/containers/components/SecondFactorForm/SecondFactorForm.ts +++ b/client/src/containers/components/SecondFactorForm/SecondFactorForm.ts @@ -55,9 +55,9 @@ async function triggerSecurityKeySigning(dispatch: Dispatch) { async function handleSuccess(dispatch: Dispatch, ownProps: OwnProps, duration?: number) { async function handle() { - if (ownProps.redirection) { + if (ownProps.redirectionUrl) { try { - await SafelyRedirectBehavior(ownProps.redirection, dispatch); + await SafelyRedirectBehavior(ownProps.redirectionUrl); } catch (e) { await fetchState(dispatch); } diff --git a/client/src/containers/views/AuthenticationView/AuthenticationView.ts b/client/src/containers/views/AuthenticationView/AuthenticationView.ts index 7aa6dec4..47203719 100644 --- a/client/src/containers/views/AuthenticationView/AuthenticationView.ts +++ b/client/src/containers/views/AuthenticationView/AuthenticationView.ts @@ -37,11 +37,9 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => { }; } -const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => { +const mapDispatchToProps = (dispatch: Dispatch) => { return { - onInit: async () => { - await FetchStateBehavior(dispatch); - } + onInit: async () => await FetchStateBehavior(dispatch) } } diff --git a/client/src/reducers/index.ts b/client/src/reducers/index.ts index bdaa1bb5..8458f523 100644 --- a/client/src/reducers/index.ts +++ b/client/src/reducers/index.ts @@ -6,6 +6,7 @@ function getReturnType (f: (...args: any[]) => R): R { } const t = getReturnType(PortalReducer) + export type RootState = StateType; export default PortalReducer; \ No newline at end of file diff --git a/client/src/services/AutheliaService.ts b/client/src/services/AutheliaService.ts index e1338529..ed77fdff 100644 --- a/client/src/services/AutheliaService.ts +++ b/client/src/services/AutheliaService.ts @@ -23,13 +23,20 @@ export async function fetchState() { } export async function postFirstFactorAuth(username: string, password: string, - rememberMe: boolean) { + rememberMe: boolean, redirectionUrl: string | null) { + + const headers: Record = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + } + + if (redirectionUrl) { + headers['X-Target-Url'] = redirectionUrl; + } + return fetchSafe('/api/firstfactor', { method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, + headers: headers, body: JSON.stringify({ username: username, password: password, diff --git a/client/src/views/AuthenticationView/AuthenticationView.tsx b/client/src/views/AuthenticationView/AuthenticationView.tsx index 0c43e0a4..6c6750f6 100644 --- a/client/src/views/AuthenticationView/AuthenticationView.tsx +++ b/client/src/views/AuthenticationView/AuthenticationView.tsx @@ -36,12 +36,12 @@ class AuthenticationView extends Component { if (this.props.stage === Stage.SECOND_FACTOR) { return ; + redirectionUrl={this.props.redirectionUrl} />; } else if (this.props.stage === Stage.ALREADY_AUTHENTICATED) { return ; } - return ; + return ; } } diff --git a/scripts/authelia-scripts-start b/scripts/authelia-scripts-start index 39f32749..fa12d345 100755 --- a/scripts/authelia-scripts-start +++ b/scripts/authelia-scripts-start @@ -1,7 +1,6 @@ #!/usr/bin/env node var program = require('commander'); -var exec = require('child_process').execSync; var spawn = require('child_process').spawn; var chokidar = require('chokidar'); var fs = require('fs'); @@ -101,15 +100,39 @@ function reload(path) { reloadServer(); } -fs.writeFileSync(ENVIRONMENT_FILENAME, program.suite); -exec('./example/compose/nginx/portal/render.js'); -exec('./scripts/utils/prepare-environment.sh'); +function exec(command, args) { + return new Promise((resolve, reject) => { + const cmd = spawn(command, args); -console.log('Start watching'); -tsWatcher.on('add', reload); -tsWatcher.on('remove', reload); -tsWatcher.on('change', reload); + cmd.stdout.pipe(process.stdout); + cmd.stderr.pipe(process.stderr); + cmd.on('close', (code) => { + if (code == 0) { + resolve(); + return; + } + reject(new Error('Status code ' + code)); + }); + }); +} -startServer(); -startClient(); +async function main() { + console.log(`Create suite file ${ENVIRONMENT_FILENAME}.`); + fs.writeFileSync(ENVIRONMENT_FILENAME, program.suite); + console.log(`Render nginx configuration...`); + await exec('./example/compose/nginx/portal/render.js'); + + console.log(`Prepare environment with docker-compose...`); + await exec('./scripts/utils/prepare-environment.sh'); + + console.log('Start watching...'); + tsWatcher.on('add', reload); + tsWatcher.on('remove', reload); + tsWatcher.on('change', reload); + + startServer(); + startClient(); +} + +main() diff --git a/server/src/lib/authorization/Authorizer.ts b/server/src/lib/authorization/Authorizer.ts index 889b7ec2..1d399562 100644 --- a/server/src/lib/authorization/Authorizer.ts +++ b/server/src/lib/authorization/Authorizer.ts @@ -1,5 +1,5 @@ -import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration"; +import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration"; import { IAuthorizer } from "./IAuthorizer"; import { Winston } from "../../../types/Dependencies"; import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index 35b7e84c..e10a29f5 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -1,5 +1,6 @@ import Exceptions = require("../../Exceptions"); +import * as ObjectPath from "object-path"; import BluebirdPromise = require("bluebird"); import express = require("express"); import ErrorReplies = require("../../ErrorReplies"); @@ -9,6 +10,11 @@ import { ServerVariables } from "../../ServerVariables"; import { AuthenticationSession } from "../../../../types/AuthenticationSession"; import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; import { Level } from "../../authentication/Level"; +import { Level as AuthorizationLevel } from "../../authorization/Level"; +import { BelongToDomain } from "../../../../../shared/BelongToDomain"; +import { URLDecomposer } from "../..//utils/URLDecomposer"; +import { Object } from "../../../lib/authorization/Object"; +import { Subject } from "../../../lib/authorization/Subject"; export default function (vars: ServerVariables) { return function (req: express.Request, res: express.Response) @@ -54,6 +60,37 @@ export default function (vars: ServerVariables) { vars.logger.debug(req, "Mark successful authentication to regulator."); vars.regulator.mark(username, true); + }) + .then(function() { + const targetUrl = ObjectPath.get(req, 'headers.x-target-url', null); + + if (!targetUrl) { + res.status(204); + res.send(); + return BluebirdPromise.resolve(); + } + + if (BelongToDomain(targetUrl, vars.config.session.domain)) { + const resource = URLDecomposer.fromUrl(targetUrl); + const resObject: Object = { + domain: resource.domain, + resource: resource.path, + } + + const subject: Subject = { + user: authSession.userid, + groups: authSession.groups + } + + const authorizationLevel = vars.authorizer.authorization(resObject, subject); + if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) { + res.json({ + redirect: targetUrl + }); + return BluebirdPromise.resolve(); + } + } + res.status(204); res.send(); return BluebirdPromise.resolve(); diff --git a/server/src/lib/routes/verify/get_session_cookie.ts b/server/src/lib/routes/verify/get_session_cookie.ts index 07034481..f703b944 100644 --- a/server/src/lib/routes/verify/get_session_cookie.ts +++ b/server/src/lib/routes/verify/get_session_cookie.ts @@ -56,8 +56,6 @@ export default function (req: Express.Request, res: Express.Response, const originalUrl = ObjectPath.get( req, "headers.x-original-url"); - const originalUri = - ObjectPath.get(req, "headers.x-original-uri"); const d = URLDecomposer.fromUrl(originalUrl); vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain, diff --git a/server/src/lib/utils/SafeRedirection.ts b/server/src/lib/utils/SafeRedirection.ts index 9e6a32e0..0a2e7048 100644 --- a/server/src/lib/utils/SafeRedirection.ts +++ b/server/src/lib/utils/SafeRedirection.ts @@ -1,5 +1,4 @@ import Express = require("express"); -import { DomainExtractor } from "../../../../shared/DomainExtractor"; import { BelongToDomain } from "../../../../shared/BelongToDomain"; diff --git a/server/src/lib/utils/URLDecomposer.ts b/server/src/lib/utils/URLDecomposer.ts index 9bdf2e9d..e86d64fc 100644 --- a/server/src/lib/utils/URLDecomposer.ts +++ b/server/src/lib/utils/URLDecomposer.ts @@ -1,3 +1,4 @@ +// TODO: replace this decompose by third party library. export class URLDecomposer { static fromUrl(url: string): {domain: string, path: string} { if (!url) return; diff --git a/test/features/forward-headers.feature b/test/features/forward-headers.feature deleted file mode 100644 index 3e45c135..00000000 --- a/test/features/forward-headers.feature +++ /dev/null @@ -1,11 +0,0 @@ -Feature: Headers are correctly forwarded to backend - @need-authenticated-user-john - Scenario: Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend - When I visit "https://public.example.com:8080/headers" - Then I see header "Custom-Forwarded-User" set to "john" - Then I see header "Custom-Forwarded-Groups" set to "dev,admin" - - Scenario: Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend when basic auth is used - When I request "https://single_factor.example.com:8080/headers" with username "john" and password "password" using basic authentication - Then I received header "Custom-Forwarded-User" set to "john" - And I received header "Custom-Forwarded-Groups" set to "dev,admin" \ No newline at end of file diff --git a/test/helpers/AccessSecret.ts b/test/helpers/AccessSecret.ts deleted file mode 100644 index 2d9bbf75..00000000 --- a/test/helpers/AccessSecret.ts +++ /dev/null @@ -1,14 +0,0 @@ -import SeleniumWebdriver from "selenium-webdriver"; - -export default async function(driver: any) { - const content = await driver.wait( - SeleniumWebdriver.until.elementLocated( - SeleniumWebdriver.By.tagName('body')), 5000).getText(); - - if (content.indexOf('This is a very important secret') > - 1) { - return; - } - else { - throw new Error('Secret page is not accessible.'); - } -} \ No newline at end of file diff --git a/test/helpers/ClickOn.ts b/test/helpers/ClickOn.ts index f8b55417..09b5935d 100644 --- a/test/helpers/ClickOn.ts +++ b/test/helpers/ClickOn.ts @@ -3,6 +3,5 @@ import SeleniumWebdriver, { WebDriver, Locator } from "selenium-webdriver"; export default async function(driver: WebDriver, locator: Locator) { const el = await driver.wait( SeleniumWebdriver.until.elementLocated(locator), 5000); - await el.click(); }; \ No newline at end of file diff --git a/test/helpers/FullLogin.ts b/test/helpers/FullLogin.ts index ca8e29b9..72ee4dcc 100644 --- a/test/helpers/FullLogin.ts +++ b/test/helpers/FullLogin.ts @@ -1,13 +1,13 @@ import VisitPage from "./VisitPage"; -import FillLoginPageWithUserAndPasswordAndClick from "./FillLoginPageAndClick"; +import FillLoginPageAndClick from "./FillLoginPageAndClick"; import ValidateTotp from "./ValidateTotp"; -import WaitRedirected from "./WaitRedirected"; +import VerifyUrlIs from "./assertions/VerifyUrlIs"; import { WebDriver } from "selenium-webdriver"; // Validate the two factors! -export default async function(driver: WebDriver, url: string, user: string, secret: string) { +export default async function(driver: WebDriver, user: string, secret: string, url: string) { await VisitPage(driver, `https://login.example.com:8080/?rd=${url}`); - await FillLoginPageWithUserAndPasswordAndClick(driver, user, 'password'); + await FillLoginPageAndClick(driver, user, 'password'); await ValidateTotp(driver, secret); - await WaitRedirected(driver, "https://admin.example.com:8080/secret.html"); + await VerifyUrlIs(driver, url); } \ No newline at end of file diff --git a/test/helpers/LoginAndRegisterTotp.ts b/test/helpers/LoginAndRegisterTotp.ts index f07a8c6c..0014970c 100644 --- a/test/helpers/LoginAndRegisterTotp.ts +++ b/test/helpers/LoginAndRegisterTotp.ts @@ -1,10 +1,10 @@ import RegisterTotp from './RegisterTotp'; import LoginAs from './LoginAs'; import { WebDriver } from 'selenium-webdriver'; -import IsSecondFactorStage from './IsSecondFactorStage'; +import VerifyIsSecondFactorStage from './assertions/VerifyIsSecondFactorStage'; -export default async function(driver: WebDriver, user: string, email?: boolean) { +export default async function(driver: WebDriver, user: string, email: boolean = false) { await LoginAs(driver, user); - await IsSecondFactorStage(driver); + await VerifyIsSecondFactorStage(driver); return await RegisterTotp(driver, email); } \ No newline at end of file diff --git a/test/helpers/LoginAs.ts b/test/helpers/LoginAs.ts index ce3286bf..30381b4f 100644 --- a/test/helpers/LoginAs.ts +++ b/test/helpers/LoginAs.ts @@ -2,7 +2,7 @@ import VisitPage from "./VisitPage"; import FillLoginPageAndClick from './FillLoginPageAndClick'; import { WebDriver } from "selenium-webdriver"; -export default async function(driver: WebDriver, user: string) { +export default async function(driver: WebDriver, user: string, password: string = "password") { await VisitPage(driver, "https://login.example.com:8080/"); - await FillLoginPageAndClick(driver, user, "password"); + await FillLoginPageAndClick(driver, user, password); } \ No newline at end of file diff --git a/test/helpers/assertions/VerifyForwardedHeaderIs.ts b/test/helpers/assertions/VerifyForwardedHeaderIs.ts new file mode 100644 index 00000000..d4caba02 --- /dev/null +++ b/test/helpers/assertions/VerifyForwardedHeaderIs.ts @@ -0,0 +1,13 @@ +import SeleniumWebDriver, { WebDriver } from "selenium-webdriver"; +import Util from "util"; + +export default async function(driver: WebDriver, header: string, expectedValue: string) { + const el = await driver.wait(SeleniumWebDriver.until.elementLocated(SeleniumWebDriver.By.tagName("body")), 5000); + const text = await el.getText(); + + const expectedLine = Util.format("\"%s\": \"%s\"", header, expectedValue); + + if (text.indexOf(expectedLine) < 0) { + throw new Error("Header not found."); + } +} \ No newline at end of file diff --git a/test/helpers/IsAlreadyAuthenticatedStage.ts b/test/helpers/assertions/VerifyIsAlreadyAuthenticatedStage.ts similarity index 100% rename from test/helpers/IsAlreadyAuthenticatedStage.ts rename to test/helpers/assertions/VerifyIsAlreadyAuthenticatedStage.ts diff --git a/test/helpers/IsSecondFactorStage.ts b/test/helpers/assertions/VerifyIsSecondFactorStage.ts similarity index 100% rename from test/helpers/IsSecondFactorStage.ts rename to test/helpers/assertions/VerifyIsSecondFactorStage.ts diff --git a/test/helpers/assertions/ObserveSecret.ts b/test/helpers/assertions/VerifySecretObserved.ts similarity index 100% rename from test/helpers/assertions/ObserveSecret.ts rename to test/helpers/assertions/VerifySecretObserved.ts diff --git a/test/helpers/assertions/VerifyUrlIs.ts b/test/helpers/assertions/VerifyUrlIs.ts new file mode 100644 index 00000000..136a5719 --- /dev/null +++ b/test/helpers/assertions/VerifyUrlIs.ts @@ -0,0 +1,5 @@ +import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; + +export default async function(driver: WebDriver, url: string, timeout: number = 5000) { + await driver.wait(SeleniumWebdriver.until.urlIs(url), timeout); +} \ No newline at end of file diff --git a/test/helpers/behaviors/LoginOneFactor.ts b/test/helpers/behaviors/LoginOneFactor.ts new file mode 100644 index 00000000..eede3cbe --- /dev/null +++ b/test/helpers/behaviors/LoginOneFactor.ts @@ -0,0 +1,17 @@ +import { WebDriver } from "selenium-webdriver"; +import LoginAndRegisterTotp from "../LoginAndRegisterTotp"; +import FullLogin from "../FullLogin"; +import VisitPage from "../VisitPage"; +import FillLoginPageAndClick from "../FillLoginPageAndClick"; +import VerifyUrlIs from "../assertions/VerifyUrlIs"; + +export default async function( + driver: WebDriver, + username: string, + password: string, + targetUrl: string) { + + await VisitPage(driver, `https://login.example.com:8080/?rd=${targetUrl}`); + await FillLoginPageAndClick(driver, username, password); + await VerifyUrlIs(driver, targetUrl); +}; \ No newline at end of file diff --git a/test/helpers/behaviors/RegisterAndLoginTwoFactor.ts b/test/helpers/behaviors/RegisterAndLoginTwoFactor.ts new file mode 100644 index 00000000..475c3732 --- /dev/null +++ b/test/helpers/behaviors/RegisterAndLoginTwoFactor.ts @@ -0,0 +1,13 @@ +import { WebDriver } from "selenium-webdriver"; +import LoginAndRegisterTotp from "../LoginAndRegisterTotp"; +import FullLogin from "../FullLogin"; + +export default async function( + driver: WebDriver, + username: string, + email: boolean = false, + targetUrl: string = "https://login.example.com:8080/") { + + const secret = await LoginAndRegisterTotp(driver, username, email); + await FullLogin(driver, username, secret, targetUrl); +}; \ No newline at end of file diff --git a/test/helpers/context/WithDriver.ts b/test/helpers/context/WithDriver.ts index c7c6960a..adb4c7f0 100644 --- a/test/helpers/context/WithDriver.ts +++ b/test/helpers/context/WithDriver.ts @@ -1,31 +1,39 @@ require("chromedriver"); import chrome from 'selenium-webdriver/chrome'; -import SeleniumWebdriver from "selenium-webdriver"; +import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; -export default function(forEach: boolean = false) { +export async function StartDriver() { let options = new chrome.Options(); if (process.env['HEADLESS'] == 'y') { options = options.headless(); } - function beforeBlock(this: Mocha.IHookCallbackContext) { - const driver = new SeleniumWebdriver.Builder() - .forBrowser("chrome") - .setChromeOptions(options) - .build(); - this.driver = driver; - } + const driver = new SeleniumWebdriver.Builder() + .forBrowser("chrome") + .setChromeOptions(options) + .build(); + return driver; +} - function afterBlock(this: Mocha.IHookCallbackContext) { - return this.driver.quit(); - } +export async function StopDriver(driver: WebDriver) { + return await driver.quit(); +} +export default function(forEach: boolean = false) { if (forEach) { - beforeEach(beforeBlock); - afterEach(afterBlock); + beforeEach(async function() { + this.driver = await StartDriver(); + }); + afterEach(async function() { + await StopDriver(this.driver); + }); } else { - before(beforeBlock); - after(afterBlock); + before(async function() { + this.driver = await StartDriver(); + }); + after(async function() { + await StopDriver(this.driver) + }); } } \ No newline at end of file diff --git a/test/suites/complete/index.ts b/test/suites/complete/index.ts index e7932b62..5dd9a689 100644 --- a/test/suites/complete/index.ts +++ b/test/suites/complete/index.ts @@ -2,10 +2,12 @@ import AutheliaSuite from "../../helpers/context/AutheliaSuite"; import MongoConnectionRecovery from "./scenarii/MongoConnectionRecovery"; import EnforceInternalRedirectionsOnly from "./scenarii/EnforceInternalRedirectionsOnly"; import AccessControl from "./scenarii/AccessControl"; +import CustomHeadersForwarded from "./scenarii/CustomHeadersForwarded"; AutheliaSuite('Complete configuration', __dirname + '/config.yml', function() { this.timeout(10000); + describe('Custom headers forwarded to backend', CustomHeadersForwarded); describe('Access control', AccessControl); describe('Mongo broken connection recovery', MongoConnectionRecovery); diff --git a/test/suites/complete/scenarii/AccessControl.ts b/test/suites/complete/scenarii/AccessControl.ts index e3cf844b..66766e2e 100644 --- a/test/suites/complete/scenarii/AccessControl.ts +++ b/test/suites/complete/scenarii/AccessControl.ts @@ -1,23 +1,23 @@ import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; import VisitPage from "../../../helpers/VisitPage"; -import ObserveSecret from "../../../helpers/assertions/ObserveSecret"; +import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; import WithDriver from "../../../helpers/context/WithDriver"; import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick"; import ValidateTotp from "../../../helpers/ValidateTotp"; -import WaitRedirected from "../../../helpers/WaitRedirected"; +import VerifyUrlIs from "../../../helpers/assertions/VerifyUrlIs"; import Logout from "../../../helpers/Logout"; async function ShouldHaveAccessTo(url: string) { it('should have access to ' + url, async function() { await VisitPage(this.driver, url); - await ObserveSecret(this.driver); + await VerifySecretObserved(this.driver); }) } async function ShouldNotHaveAccessTo(url: string) { it('should not have access to ' + url, async function() { await this.driver.get(url); - await WaitRedirected(this.driver, 'https://login.example.com:8080/'); + await VerifyUrlIs(this.driver, 'https://login.example.com:8080/'); }) } diff --git a/test/suites/complete/scenarii/CustomHeadersForwarded.ts b/test/suites/complete/scenarii/CustomHeadersForwarded.ts new file mode 100644 index 00000000..546f3439 --- /dev/null +++ b/test/suites/complete/scenarii/CustomHeadersForwarded.ts @@ -0,0 +1,51 @@ +import Logout from "../../../helpers/Logout"; +import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; +import RegisterAndLoginWith2FA from "../../../helpers/behaviors/RegisterAndLoginTwoFactor"; +import VerifyForwardedHeaderIs from "../../../helpers/assertions/VerifyForwardedHeaderIs"; +import LoginOneFactor from "../../../helpers/behaviors/LoginOneFactor"; + +export default function() { + describe("Custom-Forwarded-User and Custom-Forwarded-Groups are correctly forwarded to protected backend", function() { + this.timeout(10000); + + describe("With single factor", function() { + before(async function() { + this.driver = await StartDriver(); + await LoginOneFactor(this.driver, "john", "password", "https://single_factor.example.com:8080/headers"); + }); + + after(async function() { + await Logout(this.driver); + await StopDriver(this.driver); + }); + + it("should see header 'Custom-Forwarded-User' set to 'john'", async function() { + await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-User', 'john'); + }); + + it("should see header 'Custom-Forwarded-Groups' set to 'dev,admin'", async function() { + await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-Groups', 'dev,admin'); + }); + }); + + describe("With two factors", function() { + before(async function() { + this.driver = await StartDriver(); + await RegisterAndLoginWith2FA(this.driver, "john", true, "https://public.example.com:8080/headers"); + }); + + after(async function() { + await Logout(this.driver); + await StopDriver(this.driver); + }); + + it("should see header 'Custom-Forwarded-User' set to 'john'", async function() { + await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-User', 'john'); + }); + + it("should see header 'Custom-Forwarded-Groups' set to 'dev,admin'", async function() { + await VerifyForwardedHeaderIs(this.driver, 'Custom-Forwarded-Groups', 'dev,admin'); + }); + }); + }); +} \ No newline at end of file diff --git a/test/suites/complete/scenarii/EnforceInternalRedirectionsOnly.ts b/test/suites/complete/scenarii/EnforceInternalRedirectionsOnly.ts index a7c34b56..cd33d24a 100644 --- a/test/suites/complete/scenarii/EnforceInternalRedirectionsOnly.ts +++ b/test/suites/complete/scenarii/EnforceInternalRedirectionsOnly.ts @@ -4,8 +4,8 @@ import FillLoginPageWithUserAndPasswordAndClick from '../../../helpers/FillLogin import ValidateTotp from "../../../helpers/ValidateTotp"; import Logout from "../../../helpers/Logout"; import WaitRedirected from "../../../helpers/WaitRedirected"; -import IsAlreadyAuthenticatedStage from "../../../helpers/IsAlreadyAuthenticatedStage"; -import WithDriver from "../../../helpers/context/WithDriver"; +import IsAlreadyAuthenticatedStage from "../../../helpers/assertions/VerifyIsAlreadyAuthenticatedStage"; +import { StartDriver, StopDriver } from "../../../helpers/context/WithDriver"; /* * Authelia should not be vulnerable to open redirection. Otherwise it would aid an @@ -15,17 +15,18 @@ import WithDriver from "../../../helpers/context/WithDriver"; * the URL is pointing to an external domain. */ export default function() { - WithDriver(true); describe("Only redirection to a subdomain of the protected domain should be allowed", function() { this.timeout(10000); let secret: string; beforeEach(async function() { + this.driver = await StartDriver(); secret = await LoginAndRegisterTotp(this.driver, "john", true) }); afterEach(async function() { await Logout(this.driver); + await StopDriver(this.driver); }) function CannotRedirectTo(url: string) { diff --git a/test/suites/complete/scenarii/MongoConnectionRecovery.ts b/test/suites/complete/scenarii/MongoConnectionRecovery.ts index 1f1239e1..99611b68 100644 --- a/test/suites/complete/scenarii/MongoConnectionRecovery.ts +++ b/test/suites/complete/scenarii/MongoConnectionRecovery.ts @@ -16,6 +16,6 @@ export default function() { 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); + await FullLogin(this.driver, "john", secret, "https://admin.example.com:8080/secret.html"); }); } \ No newline at end of file diff --git a/test/suites/minimal/scenarii/Inactivity.ts b/test/suites/minimal/scenarii/Inactivity.ts index 78f202b9..9f472648 100644 --- a/test/suites/minimal/scenarii/Inactivity.ts +++ b/test/suites/minimal/scenarii/Inactivity.ts @@ -1,4 +1,3 @@ -import Bluebird = require("bluebird"); import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; import VisitPage from "../../../helpers/VisitPage"; import FillLoginPageWithUserAndPasswordAndClick from "../../../helpers/FillLoginPageAndClick"; diff --git a/test/suites/minimal/scenarii/ResetPassword.ts b/test/suites/minimal/scenarii/ResetPassword.ts index 3217c9e6..7e21ce75 100644 --- a/test/suites/minimal/scenarii/ResetPassword.ts +++ b/test/suites/minimal/scenarii/ResetPassword.ts @@ -7,7 +7,7 @@ import WaitRedirected from '../../../helpers/WaitRedirected'; import FillField from "../../../helpers/FillField"; import {GetLinkFromEmail} from "../../../helpers/GetIdentityLink"; import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick"; -import IsSecondFactorStage from "../../../helpers/IsSecondFactorStage"; +import IsSecondFactorStage from "../../../helpers/assertions/VerifyIsSecondFactorStage"; import SeeNotification from '../../../helpers/SeeNotification'; export default function() { diff --git a/test/suites/minimal/scenarii/TOTPValidation.ts b/test/suites/minimal/scenarii/TOTPValidation.ts index 99a60f5c..8bc69c97 100644 --- a/test/suites/minimal/scenarii/TOTPValidation.ts +++ b/test/suites/minimal/scenarii/TOTPValidation.ts @@ -2,7 +2,7 @@ import FillLoginPageWithUserAndPasswordAndClick from '../../../helpers/FillLogin import WaitRedirected from '../../../helpers/WaitRedirected'; import VisitPage from '../../../helpers/VisitPage'; import ValidateTotp from '../../../helpers/ValidateTotp'; -import AccessSecret from "../../../helpers/AccessSecret"; +import VerifySecretObserved from "../../../helpers/assertions/VerifySecretObserved"; import LoginAndRegisterTotp from '../../../helpers/LoginAndRegisterTotp'; import SeeNotification from '../../../helpers/SeeNotification'; import { AUTHENTICATION_TOTP_FAILED } from '../../../../shared/UserMessages'; @@ -25,7 +25,7 @@ export default function() { }); it("should access the secret", async function() { - await AccessSecret(this.driver); + await VerifySecretObserved(this.driver); }); });