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 { push } from 'connected-react-router';
|
||||||
import fetchState from '../../../behaviors/FetchStateBehavior';
|
import fetchState from '../../../behaviors/FetchStateBehavior';
|
||||||
import LogoutBehavior from '../../../behaviors/LogoutBehavior';
|
import LogoutBehavior from '../../../behaviors/LogoutBehavior';
|
||||||
|
import SafelyRedirectBehavior from '../../../behaviors/SafelyRedirectBehavior';
|
||||||
|
|
||||||
const mapStateToProps = (state: RootState): StateProps => ({
|
const mapStateToProps = (state: RootState): StateProps => ({
|
||||||
securityKeySupported: state.secondFactor.securityKeySupported,
|
securityKeySupported: state.secondFactor.securityKeySupported,
|
||||||
|
@ -52,19 +53,23 @@ async function triggerSecurityKeySigning(dispatch: Dispatch) {
|
||||||
await dispatch(securityKeySignSuccess());
|
await dispatch(securityKeySignSuccess());
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirectOnSuccess(dispatch: Dispatch, ownProps: OwnProps, duration?: number) {
|
async function handleSuccess(dispatch: Dispatch, ownProps: OwnProps, duration?: number) {
|
||||||
function redirect() {
|
async function handle() {
|
||||||
if (ownProps.redirection) {
|
if (ownProps.redirection) {
|
||||||
window.location.href = ownProps.redirection;
|
try {
|
||||||
|
await SafelyRedirectBehavior(ownProps.redirection, dispatch);
|
||||||
|
} catch (e) {
|
||||||
|
await fetchState(dispatch);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fetchState(dispatch);
|
await fetchState(dispatch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (duration) {
|
if (duration) {
|
||||||
setTimeout(redirect, duration);
|
setTimeout(handle, duration);
|
||||||
} else {
|
} else {
|
||||||
redirect();
|
await handle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +89,7 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
|
||||||
if (isU2FSupported) {
|
if (isU2FSupported) {
|
||||||
await dispatch(setSecurityKeySupported(true));
|
await dispatch(setSecurityKeySupported(true));
|
||||||
await triggerSecurityKeySigning(dispatch);
|
await triggerSecurityKeySigning(dispatch);
|
||||||
redirectOnSuccess(dispatch, ownProps, 1000);
|
await handleSuccess(dispatch, ownProps, 1000);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onOneTimePasswordValidationRequested: async (token: string) => {
|
onOneTimePasswordValidationRequested: async (token: string) => {
|
||||||
|
@ -106,7 +111,7 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
|
||||||
throw body['error'];
|
throw body['error'];
|
||||||
}
|
}
|
||||||
dispatch(oneTimePasswordVerificationSuccess());
|
dispatch(oneTimePasswordVerificationSuccess());
|
||||||
redirectOnSuccess(dispatch, ownProps);
|
await handleSuccess(dispatch, ownProps);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { connect } from 'react-redux';
|
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 { RootState } from '../../../reducers';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import AuthenticationLevel from '../../../types/AuthenticationLevel';
|
import AuthenticationLevel from '../../../types/AuthenticationLevel';
|
||||||
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
|
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
|
||||||
import { setRedirectionUrl } from '../../../reducers/Portal/Authentication/actions';
|
|
||||||
|
|
||||||
function authenticationLevelToStage(level: AuthenticationLevel): Stage {
|
function authenticationLevelToStage(level: AuthenticationLevel): Stage {
|
||||||
switch (level) {
|
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)
|
const stage = (state.authentication.remoteState)
|
||||||
? authenticationLevelToStage(state.authentication.remoteState.authentication_level)
|
? authenticationLevelToStage(state.authentication.remoteState.authentication_level)
|
||||||
: Stage.FIRST_FACTOR;
|
: 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 {
|
return {
|
||||||
redirectionUrl: state.authentication.redirectionUrl,
|
redirectionUrl: url,
|
||||||
remoteState: state.authentication.remoteState,
|
remoteState: state.authentication.remoteState,
|
||||||
stage: stage,
|
stage: stage,
|
||||||
};
|
};
|
||||||
|
@ -30,11 +39,8 @@ const mapStateToProps = (state: RootState): StateProps => {
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
|
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => {
|
||||||
return {
|
return {
|
||||||
onInit: async (redirectionUrl?: string) => {
|
onInit: async () => {
|
||||||
await FetchStateBehavior(dispatch);
|
await FetchStateBehavior(dispatch);
|
||||||
if (redirectionUrl) {
|
|
||||||
await dispatch(setRedirectionUrl(redirectionUrl));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,4 +114,24 @@ export async function resetPassword(newPassword: string) {
|
||||||
},
|
},
|
||||||
body: JSON.stringify({password: newPassword})
|
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 FirstFactorForm from "../../containers/components/FirstFactorForm/FirstFactorForm";
|
||||||
import SecondFactorForm from "../../containers/components/SecondFactorForm/SecondFactorForm";
|
import SecondFactorForm from "../../containers/components/SecondFactorForm/SecondFactorForm";
|
||||||
import RemoteState from "./RemoteState";
|
import RemoteState from "./RemoteState";
|
||||||
import { RouterProps } from "react-router";
|
import { RouterProps, RouteProps } from "react-router";
|
||||||
import queryString from 'query-string';
|
|
||||||
|
|
||||||
export enum Stage {
|
export enum Stage {
|
||||||
FIRST_FACTOR,
|
FIRST_FACTOR,
|
||||||
|
@ -12,6 +11,8 @@ export enum Stage {
|
||||||
ALREADY_AUTHENTICATED,
|
ALREADY_AUTHENTICATED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OwnProps extends RouteProps {}
|
||||||
|
|
||||||
export interface StateProps {
|
export interface StateProps {
|
||||||
stage: Stage;
|
stage: Stage;
|
||||||
remoteState: RemoteState | null;
|
remoteState: RemoteState | null;
|
||||||
|
@ -19,19 +20,13 @@ export interface StateProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DispatchProps {
|
export interface DispatchProps {
|
||||||
onInit: (redirectionUrl?: string) => void;
|
onInit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Props = StateProps & DispatchProps & RouterProps;
|
export type Props = StateProps & DispatchProps & RouterProps;
|
||||||
|
|
||||||
class AuthenticationView extends Component<Props> {
|
class AuthenticationView extends Component<Props> {
|
||||||
componentDidMount() {
|
componentWillMount() {
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.props.onInit();
|
this.props.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
port: 9091
|
port: 9091
|
||||||
|
|
||||||
|
log_level: debug
|
||||||
|
|
||||||
authentication_backend:
|
authentication_backend:
|
||||||
file:
|
file:
|
||||||
path: ./users_database.yml
|
path: ./users_database.yml
|
||||||
|
|
|
@ -532,6 +532,11 @@
|
||||||
"integrity": "sha512-vOVmaruQG5EatOU/jM6yU2uCp3Lz6mK1P5Ztu4iJjfM4SVHU9XYktPUQtKlIXuahqXHdEyUarMrBEwg5Cwu+bA==",
|
"integrity": "sha512-vOVmaruQG5EatOU/jM6yU2uCp3Lz6mK1P5Ztu4iJjfM4SVHU9XYktPUQtKlIXuahqXHdEyUarMrBEwg5Cwu+bA==",
|
||||||
"dev": true
|
"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": {
|
"@types/winston": {
|
||||||
"version": "2.3.9",
|
"version": "2.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/winston/-/winston-2.3.9.tgz",
|
||||||
|
@ -7053,6 +7058,11 @@
|
||||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
|
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
|
||||||
"dev": true
|
"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": {
|
"random-bytes": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
|
||||||
|
@ -7424,6 +7434,11 @@
|
||||||
"semver": "5.5.0"
|
"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": {
|
"resolve": {
|
||||||
"version": "1.7.1",
|
"version": "1.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz",
|
"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": {
|
"url-parse-lax": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"title": "Authelia API documentation"
|
"title": "Authelia API documentation"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/url-parse": "^1.4.2",
|
||||||
"ajv": "^6.3.0",
|
"ajv": "^6.3.0",
|
||||||
"bluebird": "^3.5.0",
|
"bluebird": "^3.5.0",
|
||||||
"body-parser": "^1.15.2",
|
"body-parser": "^1.15.2",
|
||||||
|
@ -50,6 +51,7 @@
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
"u2f": "^0.1.2",
|
"u2f": "^0.1.2",
|
||||||
"u2f-api": "^1.0.7",
|
"u2f-api": "^1.0.7",
|
||||||
|
"url-parse": "^1.4.4",
|
||||||
"winston": "^2.3.1",
|
"winston": "^2.3.1",
|
||||||
"yamljs": "^0.3.0"
|
"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 FirstFactorPost = require("../routes/firstfactor/post");
|
||||||
import LogoutPost from "../routes/logout/post";
|
import LogoutPost from "../routes/logout/post";
|
||||||
import StateGet from "../routes/state/get";
|
import StateGet from "../routes/state/get";
|
||||||
|
import RedirectPost from "../routes/redirect/post";
|
||||||
import VerifyGet = require("../routes/verify/get");
|
import VerifyGet = require("../routes/verify/get");
|
||||||
import TOTPSignGet = require("../routes/secondfactor/totp/sign/post");
|
import TOTPSignGet = require("../routes/secondfactor/totp/sign/post");
|
||||||
|
|
||||||
|
@ -86,6 +87,7 @@ function setupResetPassword(app: Express.Application, vars: ServerVariables) {
|
||||||
export class RestApi {
|
export class RestApi {
|
||||||
static setup(app: Express.Application, vars: ServerVariables): void {
|
static setup(app: Express.Application, vars: ServerVariables): void {
|
||||||
app.get(Endpoints.STATE_GET, StateGet(vars));
|
app.get(Endpoints.STATE_GET, StateGet(vars));
|
||||||
|
app.post(Endpoints.REDIRECT_POST, RedirectPost(vars));
|
||||||
|
|
||||||
app.post(Endpoints.LOGOUT_POST, LogoutPost(vars));
|
app.post(Endpoints.LOGOUT_POST, LogoutPost(vars));
|
||||||
|
|
||||||
|
|
|
@ -287,3 +287,17 @@ export const VERIFY_GET = "/api/verify";
|
||||||
*/
|
*/
|
||||||
export const LOGOUT_POST = "/api/logout";
|
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;
|
let running = false;
|
||||||
|
|
||||||
interface AutheliaSuiteType {
|
interface AutheliaSuiteType {
|
||||||
(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void): Mocha.ISuite;
|
(description: string, configPath: string, cb: (this: Mocha.ISuiteCallbackContext) => void): Mocha.ISuite;
|
||||||
only: (description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite;
|
only: (description: string, configPath: string, cb: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AutheliaSuiteBase(description: string,
|
function AutheliaSuiteBase(description: string, configPath: string,
|
||||||
context: (description: string, ctx: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite,
|
cb: (this: Mocha.ISuiteCallbackContext) => void,
|
||||||
cb: (this: Mocha.ISuiteCallbackContext) => void) {
|
context: (description: string, ctx: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite) {
|
||||||
if (!running && process.env['WITH_SERVER'] == 'y') {
|
if (!running && process.env['WITH_SERVER'] == 'y') {
|
||||||
WithAutheliaRunning();
|
WithAutheliaRunning(configPath);
|
||||||
running = true;
|
running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,13 +22,16 @@ function AutheliaSuiteBase(description: string,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutheliaSuite = <AutheliaSuiteType>function(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) {
|
const AutheliaSuite = <AutheliaSuiteType>function(
|
||||||
return AutheliaSuiteBase(description, describe, cb);
|
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) {
|
AutheliaSuite.only = function(description: string, configPath: string,
|
||||||
return AutheliaSuiteBase(description, describe.only, cb);
|
cb: (this: Mocha.ISuiteCallbackContext) => void) {
|
||||||
|
return AutheliaSuiteBase(description, configPath, cb, describe.only);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AutheliaSuite as AutheliaSuiteType;
|
export default AutheliaSuite as AutheliaSuiteType;
|
|
@ -1,12 +1,12 @@
|
||||||
|
|
||||||
import ChildProcess from 'child_process';
|
import ChildProcess from 'child_process';
|
||||||
|
|
||||||
export default function WithAutheliaRunning(waitTimeout: number = 3000) {
|
export default function WithAutheliaRunning(configPath: string, waitTimeout: number = 3000) {
|
||||||
before(function() {
|
before(function() {
|
||||||
this.timeout(5000);
|
this.timeout(5000);
|
||||||
const authelia = ChildProcess.spawn(
|
const authelia = ChildProcess.spawn(
|
||||||
'./scripts/authelia-scripts',
|
'./scripts/authelia-scripts',
|
||||||
['serve', '--no-watch', '--config', 'config.minimal.yml'],
|
['serve', '--no-watch', '--config', configPath],
|
||||||
{detached: true});
|
{detached: true});
|
||||||
this.authelia = authelia;
|
this.authelia = authelia;
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,7 @@ export default function() {
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
const driver = new SeleniumWebdriver.Builder()
|
const driver = new SeleniumWebdriver.Builder()
|
||||||
.forBrowser("chrome")
|
.forBrowser("chrome")
|
||||||
.setChromeOptions(
|
.setChromeOptions(new chrome.Options().headless())
|
||||||
new chrome.Options().headless())
|
|
||||||
.build();
|
.build();
|
||||||
this.driver = driver;
|
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);
|
const execAsync = Bluebird.promisify(ChildProcess.exec);
|
||||||
|
|
||||||
AutheliaSuite('Minimal configuration', function() {
|
AutheliaSuite('Minimal configuration', 'config.minimal.yml', function() {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
return execAsync("cp users_database.example.yml users_database.yml");
|
return execAsync("cp users_database.example.yml users_database.yml");
|
||||||
|
|
Loading…
Reference in New Issue