Fix e2e test with minimal configuration.

pull/330/head
Clement Michaud 2019-01-30 16:47:03 +01:00
parent 561578dffc
commit c5eb86e0fd
72 changed files with 2065 additions and 1945 deletions

View File

@ -12,7 +12,7 @@ RUN apk --update add --no-cache --virtual \
COPY dist/server /usr/src/server
COPY dist/shared /usr/src/shared
EXPOSE 8080
EXPOSE 9091
VOLUME /etc/authelia
VOLUME /var/lib/authelia

View File

@ -1,4 +1,5 @@
import React, { Component } from "react";
import classnames from 'classnames';
import styles from '../../assets/scss/components/AlreadyAuthenticated/AlreadyAuthenticated.module.scss';
import Button from "@material/react-button";
@ -17,7 +18,7 @@ export type Props = OwnProps & DispatchProps;
class AlreadyAuthenticated extends Component<Props> {
render() {
return (
<div className={styles.container}>
<div className={classnames(styles.container, 'already-authenticated-step')}>
<div className={styles.successContainer}>
<div className={styles.messageContainer}>
<span className={styles.username}>{this.props.username}</span>

View File

@ -65,7 +65,7 @@ class FirstFactorForm extends Component<Props, State> {
render() {
return (
<div>
<div className='first-factor-step'>
<Notification
show={this.props.error != null}
className={styles.notification}>
@ -103,8 +103,9 @@ class FirstFactorForm extends Component<Props, State> {
<div className={styles.buttons}>
<Button
onClick={this.onLoginClicked}
color="primary"
color='primary'
raised={true}
id='login-button'
disabled={this.props.formDisabled}>
Login
</Button>

View File

@ -11,7 +11,7 @@ interface Props {
class Notification extends Component<Props> {
render() {
return (this.props.show)
? (<div className={classnames(styles.container, this.props.className)}>
? (<div className={classnames(styles.container, this.props.className, 'notification')}>
{this.props.children}
</div>)
: null;

View File

@ -1,4 +1,5 @@
import React, { Component, KeyboardEvent, ChangeEvent, FormEvent } from 'react';
import React, { Component, KeyboardEvent, FormEvent } from 'react';
import classnames from 'classnames';
import TextField, { Input } from '@material/react-text-field';
import Button from '@material/react-button';
@ -63,7 +64,7 @@ class SecondFactorView extends Component<Props, State> {
<CircleLoader status={u2fStatus}></CircleLoader>
</div>
<div className={styles.registerDeviceContainer}>
<a className={styles.registerDevice} href="#"
<a className={classnames(styles.registerDevice, 'register-u2f')} href="#"
onClick={this.props.onRegisterSecurityKeyClicked}>
Register device
</a>
@ -89,7 +90,7 @@ class SecondFactorView extends Component<Props, State> {
private renderTotp(n: number) {
return (
<div className={styles.methodTotp} key='totp-method'>
<div className={classnames(styles.methodTotp, 'second-factor-step')} key='totp-method'>
<div className={styles.methodName}>Option {n} - One-Time Password</div>
<Notification show={this.props.oneTimePasswordVerificationError !== null}>
{this.props.oneTimePasswordVerificationError}
@ -99,14 +100,14 @@ class SecondFactorView extends Component<Props, State> {
label="One-Time Password"
outlined={true}>
<Input
name="totp-token"
id="totp-token"
name='totp-token'
id='totp-token'
onChange={this.onOneTimePasswordChanged as any}
onKeyPress={this.onTotpKeyPressed}
value={this.state.oneTimePassword} />
</TextField>
<div className={styles.registerDeviceContainer}>
<a className={styles.registerDevice} href="#"
<a className={classnames(styles.registerDevice, 'register-totp')} href="#"
onClick={this.props.onRegisterOneTimePasswordClicked}>
Register device
</a>
@ -115,6 +116,7 @@ class SecondFactorView extends Component<Props, State> {
<Button
color="primary"
raised={true}
id='totp-button'
onClick={this.onOneTimePasswordValidationRequested}
disabled={this.props.oneTimePasswordVerificationInProgress}>
OK

View File

@ -51,9 +51,10 @@ class ForgotPasswordView extends Component<Props, State> {
<TextField
className={styles.field}
outlined={true}
id="username"
label="Username">
<Input
<Input
id="username"
name="username"
onChange={this.onUsernameChanged}
onKeyPress={this.onKeyPressed}
value={this.state.username}
@ -64,6 +65,7 @@ class ForgotPasswordView extends Component<Props, State> {
<Button
onClick={this.onPasswordResetRequested}
color="primary"
id="next-button"
raised={true}
className={styles.buttonConfirm}
disabled={this.props.disabled}>
@ -75,6 +77,7 @@ class ForgotPasswordView extends Component<Props, State> {
onClick={this.props.onCancelClicked}
color="primary"
raised={true}
id="cancel-button"
className={styles.buttonCancel}>
Cancel
</Button>

View File

@ -1,4 +1,5 @@
import React, { Component } from "react";
import classnames from 'classnames';
import Button from "@material/react-button";
@ -54,10 +55,10 @@ class OneTimePasswordRegistrationView extends Component<Props> {
Register your device by scanning the barcode or adding the key.
</div>
<div className={styles.secretContainer}>
<div className={styles.qrcodeContainer}>
<div className={classnames(styles.qrcodeContainer, 'qrcode')}>
<QRCode value={secret.otpauth_url} size={180} level="Q"></QRCode>
</div>
<div className={styles.base32Container}>{secret.base32_secret}</div>
<div className={classnames(styles.base32Container, 'base32-secret')}>{secret.base32_secret}</div>
</div>
<div className={styles.loginButtonContainer}>
<Button

View File

@ -92,6 +92,7 @@ class ResetPasswordView extends Component<Props, State> {
<Input
type="password"
key="password1"
name="password1"
value={this.state.password1}
onChange={this.onPassword1Changed}
disabled={this.props.disabled}/>
@ -104,6 +105,7 @@ class ResetPasswordView extends Component<Props, State> {
<Input
type="password"
key="password2"
name="password2"
value={this.state.password2}
onKeyPress={this.onKeyPressed}
onChange={this.onPassword2Changed}
@ -114,6 +116,7 @@ class ResetPasswordView extends Component<Props, State> {
<Button
onClick={this.onResetClicked}
color="primary"
id="reset-button"
raised={true}
disabled={this.props.disabled}
className={classnames(styles.button, styles.buttonReset)}>
@ -124,6 +127,7 @@ class ResetPasswordView extends Component<Props, State> {
<Button
onClick={this.props.onCancelClicked}
color="primary"
id="cancel-button"
raised={true}
className={classnames(styles.button, styles.buttonCancel)}>
Cancel

View File

@ -2,9 +2,11 @@
# Authelia minimal configuration #
###############################################################
port: 9091
authentication_backend:
file:
path: /etc/authelia/users_database.yml
path: ./users_database.yml
session:
secret: unsecure_session_secret
@ -96,7 +98,7 @@ notifier:
username: test
password: password
secure: false
host: 'smtp'
host: 127.0.0.1
port: 1025
sender: admin@example.com

View File

@ -3,7 +3,7 @@
###############################################################
# The port to listen on
port: 8080
port: 9091
# Log level
#
@ -42,7 +42,7 @@ authentication_backend:
# production.
ldap:
# The url of the ldap server
url: ldap://openldap
url: ldap://127.0.0.1
# The base dn for every entries
base_dn: dc=example,dc=com
@ -198,7 +198,7 @@ session:
# The redis connection details
redis:
host: redis
host: 127.0.0.1
port: 6379
password: authelia
@ -229,7 +229,7 @@ storage:
# Settings to connect to mongo server
mongo:
url: mongodb://mongo
url: mongodb://127.0.0.1
database: authelia
auth:
username: authelia
@ -258,6 +258,6 @@ notifier:
username: test
password: password
secure: false
host: 'smtp'
host: 127.0.0.1
port: 1025
sender: admin@example.com

View File

@ -1,5 +1,9 @@
version: '2'
services: {}
# services: {}
networks:
authelianet:
external: true
driver: bridge
ipam:
config:
- subnet: 192.168.240.0/24
gateway: 192.168.240.1

View File

@ -111,7 +111,7 @@ http {
server_name public.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend;
set $upstream_headers http://httpbin:8000/headers;
@ -179,7 +179,7 @@ http {
server_name admin.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt;
@ -229,7 +229,7 @@ http {
server_name dev.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt;
@ -279,7 +279,7 @@ http {
server_name mx1.mail.example.com mx2.mail.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt;
@ -329,7 +329,7 @@ http {
server_name single_factor.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_verify http://authelia:8080/api/verify;
set $upstream_verify http://192.168.240.1:9091/api/verify;
set $upstream_endpoint http://nginx-backend;
set $upstream_headers http://httpbin:8000/headers;
@ -400,7 +400,7 @@ http {
server_name authelia.example.com;
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://authelia:8080;
set $upstream_endpoint http://192.168.240.1:9091;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;

View File

@ -17,6 +17,14 @@ if (program.production) {
options['production'] = true;
}
html = ejs.renderFile(__dirname + '/nginx.conf.ejs', options, (err, conf) => {
fs.writeFileSync(__dirname + '/nginx.conf', conf);
const templatePath = __dirname + '/nginx.conf.ejs';
const outputPath = __dirname + '/nginx.conf';
html = ejs.renderFile(templatePath, options, (err, conf) => {
try {
var fd = fs.openSync(outputPath, 'w');
fs.writeFileSync(fd, conf);
} catch (e) {
fs.writeFileSync(outputPath, conf);
}
});

2898
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -93,7 +93,7 @@
"grunt-run": "^0.8.0",
"istanbul": "^0.4.5",
"jquery": "^3.2.1",
"mocha": "^5.0.5",
"mocha": "^5.2.0",
"mockdate": "^2.0.1",
"nodemon": "^1.18.9",
"nyc": "^13.1.0",
@ -107,7 +107,7 @@
"tmp": "0.0.33",
"ts-node": "^6.0.1",
"tslint": "^5.2.0",
"typescript": "^2.3.2",
"typescript": "^2.9.2",
"typescript-json-schema": "^0.23.0"
},
"nyc": {

View File

@ -4,6 +4,7 @@ var program = require('commander');
program
.version('0.0.1')
.command('start', 'Start development environment.')
.command('build', 'Build production version of Authelia from source.')
.command('clean', 'Clean the production version of Authelia.')
@ -12,7 +13,6 @@ program
.command('build-docker', 'Build Docker image containing production version of Authelia.')
.command('publish-docker', 'Publish Docker image containing production version of Authelia to Dockerhub.')
program.parse(process.argv);
.parse(process.argv);

View File

@ -1,8 +1,39 @@
#!/bin/bash
#!/usr/bin/env node
# Render the production version of the nginx portal configuration
./example/compose/nginx/portal/render.js --production
var program = require('commander');
var execSync = require('child_process').execSync;
var spawn = require('child_process').spawn;
program
.option('-c, --config <config>', 'Configuration file to run Authelia with.')
.option('--no-watch', 'Disable hot reload.')
.parse(process.argv);
./scripts/utils/prepare-environment.sh
let config = 'config.yml'; // set default config file.
./node_modules/.bin/nodemon -e yml --exec 'node dist/server/src/index.js config.yml'
if (program.config) {
config = program.config;
}
// Render the production version of the nginx portal configuration
execSync('./example/compose/nginx/portal/render.js --production');
// Prepare the environment
execSync('./scripts/utils/prepare-environment.sh');
var server;
if (program.watch) {
server = spawn('./node_modules/.bin/nodemon',
['-e', 'yml', '--ignore', './users_database*.yml', '--exec', `node dist/server/src/index.js ${config}`]);
}
else {
server = spawn('/usr/bin/env', ['node', 'dist/server/src/index.js', config]);
}
server.stdout.on('data', (data) => {
process.stdout.write(`${data}`);
});
server.stderr.on('data', (data) => {
process.stderr.write(`${data}`);
});

View File

@ -1,10 +1,20 @@
#!/bin/bash
config_path=$1
if [ "$config_path" == "" ];
then
echo "Please provide a configuration file."
exit 1
fi
./example/compose/nginx/portal/render.js
./scripts/utils/prepare-environment.sh
server_watcher="./node_modules/.bin/nodemon -e yml,js,ts,json --exec ./scripts/run-dev-server.sh 2>&1 /tmp/authelia-server.log"
./node_modules/.bin/typescript-json-schema -o server/src/lib/configuration/Configuration.schema.json --strictNullChecks --required server/tsconfig.json Configuration
server_watcher="./node_modules/.bin/nodemon -e yml,js,ts,json --exec ./scripts/run-dev-server.sh $config_path 2>&1 /tmp/authelia-server.log"
client_watcher="cd client && BROWSER=none npm run start > /tmp/authelia-client.log"
./node_modules/.bin/concurrently "$server_watcher" "$client_watcher"

View File

@ -1,3 +1,25 @@
#!/bin/bash
#!/usr/bin/env node
TS_NODE_PROJECT=server/tsconfig.json ./node_modules/.bin/mocha --colors --require ts-node/register server/src/**/*.spec.ts
var program = require('commander');
var spawn = require('child_process').spawn;
program
.option('--with-server', 'Spawn Authelia before running the tests.')
.parse(process.argv);
mocha = spawn('./node_modules/.bin/mocha', ['--exit', '--colors', '--require', 'ts-node/register', ...program.args], {
env: {
...process.env,
TS_NODE_PROJECT: 'test/tsconfig.json',
WITH_SERVER: (program.withServer) ? 'y' : 'n',
}
});
mocha.stdout.on('data', (data) => {
process.stdout.write(`${data}`);
});
mocha.stderr.on('data', (data) => {
process.stderr.write(`${data}`);
});
// TS_NODE_PROJECT=server/tsconfig.json ./node_modules/.bin/mocha --colors --require ts-node/register server/src/**/*.spec.ts

View File

@ -1,3 +1,3 @@
#!/bin/bash
./node_modules/.bin/mocha --colors --require ts-node/register $*
WITH_SERVER=n TS_NODE_PROJECT=test/tsconfig.json ./node_modules/.bin/mocha --exit --colors --require ts-node/register $*

View File

@ -1,3 +1,3 @@
#!/bin/sh
./node_modules/.bin/ts-node -P ./server/tsconfig.json ./server/src/index.ts ./config.yml
./node_modules/.bin/ts-node -P ./server/tsconfig.json ./server/src/index.ts $*

View File

@ -1,14 +1,4 @@
#!/bin/bash
bridge_exists=`docker network ls | grep " authelianet " | wc -l`
if [ "$bridge_exists" != "1" ];
then
docker network create -d bridge --subnet 192.168.240.0/24 --gateway 192.168.240.1 authelianet
else
echo "Bridge authelianet already exist."
fi
./scripts/dc-dev.sh up -d
./scripts/dc-dev.sh kill -s SIGHUP nginx-portal

View File

@ -1,9 +1,9 @@
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 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';
/*

View File

@ -1,6 +1,6 @@
import WithDriver from '../helpers/with-driver';
import fullLogin from '../helpers/full-login';
import loginAndRegisterTotp from '../helpers/login-and-register-totp';
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);

View File

@ -0,0 +1,14 @@
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.');
}
}

View File

@ -0,0 +1,8 @@
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();
};

View File

@ -0,0 +1,8 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, linkText: string) {
const element = await driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.linkText(linkText)), 5000)
await element.click();
};

View File

@ -0,0 +1,9 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, fieldName: string, text: string) {
const element = await driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.name(fieldName)), 5000)
await element.sendKeys(text);
};

View File

@ -0,0 +1,17 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(
driver: WebDriver,
username: string,
password: string,
keepMeLoggedIn: boolean = false) {
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("username")), 5000)
await driver.findElement(SeleniumWebdriver.By.id("username")).sendKeys(username);
await driver.findElement(SeleniumWebdriver.By.id("password")).sendKeys(password);
if (keepMeLoggedIn) {
await driver.findElement(SeleniumWebdriver.By.id("keep_me_logged_in")).click();
return;
}
await driver.findElement(SeleniumWebdriver.By.tagName("button")).click();
};

View File

@ -0,0 +1,13 @@
import VisitPage from "./VisitPage";
import FillLoginPageWithUserAndPasswordAndClick from "./FillLoginPageAndClick";
import ValidateTotp from "./ValidateTotp";
import WaitRedirected from "./WaitRedirected";
import { WebDriver } from "selenium-webdriver";
// Validate the two factors!
export default async function(driver: WebDriver, url: string, user: string, secret: string) {
await VisitPage(driver, `https://login.example.com:8080/?rd=${url}`);
await FillLoginPageWithUserAndPasswordAndClick(driver, user, 'password');
await ValidateTotp(driver, secret);
await WaitRedirected(driver, "https://admin.example.com:8080/secret.html");
}

View File

@ -0,0 +1,32 @@
import Bluebird = require("bluebird");
import Fs = require("fs");
import Request = require("request-promise");
export async function GetLinkFromFile() {
const data = await Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt")
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(data.toLocaleString());
if (match == null) {
throw new Error('No match');
}
return match[1];
};
export async function GetLinkFromEmail() {
const data = await Request({
method: "GET",
uri: "http://localhost:8085/messages",
json: true
});
const messageId = data[data.length - 1].id;
const data2 = await Request({
method: "GET",
uri: `http://localhost:8085/messages/${messageId}.html`
});
const regexp = new RegExp(/<a href="(.+)" class="button">Continue<\/a>/);
const match = regexp.exec(data2);
if (match == null) {
throw new Error('No match');
}
return match[1];
};

View File

@ -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('second-factor-step')));
}

View File

@ -0,0 +1,10 @@
import RegisterTotp from './RegisterTotp';
import LoginAs from './LoginAs';
import { WebDriver } from 'selenium-webdriver';
import IsSecondFactorStage from './IsSecondFactorStage';
export default async function(driver: WebDriver, user: string, email?: boolean) {
await LoginAs(driver, user);
await IsSecondFactorStage(driver);
return await RegisterTotp(driver, email);
}

View File

@ -0,0 +1,8 @@
import VisitPage from "./VisitPage";
import FillLoginPageAndClick from './FillLoginPageAndClick';
import { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, user: string) {
await VisitPage(driver, "https://login.example.com:8080/");
await FillLoginPageAndClick(driver, user, "password");
}

View File

@ -0,0 +1,13 @@
import SeleniumWebdriver = require("selenium-webdriver");
import {GetLinkFromFile, GetLinkFromEmail} from './GetIdentityLink';
export default async function(driver: SeleniumWebdriver.WebDriver, email?: boolean){
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), 5000)
await driver.findElement(SeleniumWebdriver.By.className("register-totp")).click();
await driver.sleep(500);
const link = (email) ? await GetLinkFromEmail() : await GetLinkFromFile();
await driver.get(link);
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("base32-secret")), 5000);
return await driver.findElement(SeleniumWebdriver.By.className("base32-secret")).getText();
};

View File

@ -0,0 +1,9 @@
import SeleniumWebdriver, { ThenableWebDriver, WebDriver } from "selenium-webdriver";
import Assert = require("assert");
export default async function(driver: WebDriver, type: string, message: string) {
await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("notification")), 5000)
const notificationEl = driver.findElement(SeleniumWebdriver.By.className("notification"));
const txt = await notificationEl.getText();
Assert.equal(message, txt);
}

View File

@ -0,0 +1,16 @@
import Speakeasy from "speakeasy";
import SeleniumWebdriver, { WebDriver } from 'selenium-webdriver';
export default async function(driver: WebDriver, secret: string) {
const token = Speakeasy.totp({
secret: secret,
encoding: "base32"
});
await driver.wait(SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.id("totp-token")), 5000)
await driver.findElement(SeleniumWebdriver.By.id("totp-token")).sendKeys(token);
const el = await driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id('totp-button')));
el.click();
}

View File

@ -0,0 +1,6 @@
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
export default async function(driver: WebDriver, url: string, timeout: number = 5000) {
await driver.get(url)
await driver.wait(SeleniumWebdriver.until.urlIs(url), timeout);
}

View File

@ -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);
}

View File

@ -1,12 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
import Bluebird = require("bluebird");
export default function(driver: any) {
return driver.findElement(
SeleniumWebdriver.By.tagName('h1')).getText()
.then(function(content: string) {
return (content.indexOf('Secret') > -1)
? Bluebird.resolve()
: Bluebird.reject(new Error("Secret is not accessible."));
})
}

View File

@ -1,13 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, buttonText: string) {
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.tagName("button")), 5000)
.then(function () {
return driver
.findElement(SeleniumWebdriver.By.tagName("button"))
.findElement(SeleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click();
});
};

View File

@ -1,10 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, linkText: string) {
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.linkText(linkText)), 5000)
.then(function (el) {
return el.click();
});
};

View File

@ -0,0 +1,34 @@
import WithAutheliaRunning from "./WithAutheliaRunning";
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;
}
function AutheliaSuiteBase(description: string,
context: (description: string, ctx: (this: Mocha.ISuiteCallbackContext) => void) => Mocha.ISuite,
cb: (this: Mocha.ISuiteCallbackContext) => void) {
if (!running && process.env['WITH_SERVER'] == 'y') {
WithAutheliaRunning();
running = true;
}
return context('Suite: ' + description, function(this: Mocha.ISuiteCallbackContext) {
WithDriver.call(this);
cb.call(this);
});
}
const AutheliaSuite = <AutheliaSuiteType>function(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) {
return AutheliaSuiteBase(description, describe, cb);
}
AutheliaSuite.only = function(description: string, cb: (this: Mocha.ISuiteCallbackContext) => void) {
return AutheliaSuiteBase(description, describe.only, cb);
}
export default AutheliaSuite as AutheliaSuiteType;

View File

@ -0,0 +1,23 @@
import ChildProcess from 'child_process';
export default function WithAutheliaRunning(waitTimeout: number = 3000) {
before(function() {
this.timeout(5000);
const authelia = ChildProcess.spawn(
'./scripts/authelia-scripts',
['serve', '--no-watch', '--config', 'config.minimal.yml'],
{detached: true});
this.authelia = authelia;
const waitPromise = new Promise((resolve, reject) => setTimeout(() => resolve(), waitTimeout));
return waitPromise;
});
after(function() {
this.timeout(1000);
// Kill the group of processes.
process.kill(-this.authelia.pid);
});
}

View File

@ -0,0 +1,20 @@
require("chromedriver");
import chrome from 'selenium-webdriver/chrome';
import SeleniumWebdriver from "selenium-webdriver";
export default function() {
const options = new chrome.Options().addArguments('headless');
beforeEach(function() {
const driver = new SeleniumWebdriver.Builder()
.forBrowser("chrome")
.setChromeOptions(
new chrome.Options().headless())
.build();
this.driver = driver;
});
afterEach(function() {
this.driver.quit();
});
}

View File

@ -1,10 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, fieldName: string, text: string) {
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.name(fieldName)), 5000)
.then(function (el) {
return el.sendKeys(text);
});
};

View File

@ -1,29 +0,0 @@
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
export default function(
driver: any,
username: string,
password: string,
keepMeLoggedIn: boolean = false) {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("username")), 5000)
.then(() => {
return driver.findElement(SeleniumWebdriver.By.id("username"))
.sendKeys(username);
})
.then(() => {
return driver.findElement(SeleniumWebdriver.By.id("password"))
.sendKeys(password);
})
.then(() => {
if (keepMeLoggedIn) {
return driver.findElement(SeleniumWebdriver.By.id("keep_me_logged_in"))
.click();
}
return Bluebird.resolve();
})
.then(() => {
return driver.findElement(SeleniumWebdriver.By.tagName("button"))
.click();
});
};

View File

@ -1,12 +0,0 @@
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"));
}

View File

@ -1,34 +0,0 @@
import Bluebird = require("bluebird");
import Fs = require("fs");
import Request = require("request-promise");
export function GetLinkFromFile(): Bluebird<string> {
return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt")
.then(function (data: any) {
const regexp = new RegExp(/Link: (.+)/);
const match = regexp.exec(data);
const link = match[1];
return Bluebird.resolve(link);
});
};
export function GetLinkFromEmail(): Bluebird<string> {
return Request({
method: "GET",
uri: "http://localhost:8085/messages",
json: true
})
.then(function (data: any) {
const messageId = data[data.length - 1].id;
return Request({
method: "GET",
uri: `http://localhost:8085/messages/${messageId}.html`
});
})
.then(function (data: any) {
const regexp = new RegExp(/<a href="(.+)" class="button">Continue<\/a>/);
const match = regexp.exec(data);
const link = match[1];
return Bluebird.resolve(link);
});
};

View File

@ -1,10 +0,0 @@
import RegisterTotp from './register-totp';
import WaitRedirected from './wait-redirected';
import LoginAs from './login-as';
import Bluebird = require("bluebird");
export default function(driver: any, user: string, email?: boolean): Bluebird<string> {
return LoginAs(driver, user)
.then(() => WaitRedirected(driver, "https://login.example.com:8080/secondfactor"))
.then(() => RegisterTotp(driver, email));
}

View File

@ -1,7 +0,0 @@
import VisitPage from "./visit-page";
import FillLoginPageAndClick from './fill-login-page-and-click';
export default function(driver: any, user: string) {
return VisitPage(driver, "https://login.example.com:8080/")
.then(() => FillLoginPageAndClick(driver, user, "password"));
}

View File

@ -1,23 +0,0 @@
import Bluebird = require("bluebird");
import SeleniumWebdriver = require("selenium-webdriver");
import {GetLinkFromFile, GetLinkFromEmail} from '../helpers/get-identity-link';
export default function(driver: any, email?: boolean): Bluebird<string> {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), 5000)
.then(function () {
return driver.findElement(SeleniumWebdriver.By.className("register-totp")).click();
})
.then(function () {
if(email) return GetLinkFromEmail();
else return GetLinkFromFile();
})
.then(function (link: string) {
return driver.get(link);
})
.then(function () {
return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.id("secret")), 5000);
})
.then(function () {
return driver.findElement(SeleniumWebdriver.By.id("secret")).getText();
});
};

View File

@ -1,18 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
export default function(driver: any, type: string, message: string) {
const notificationEl = driver.findElement(SeleniumWebdriver.By.className("notification"));
return driver.wait(SeleniumWebdriver.until.elementIsVisible(notificationEl), 5000)
.then(function () {
return notificationEl.getText();
})
.then(function (txt: string) {
Assert.equal(message, txt);
return notificationEl.getAttribute("class");
})
.then(function (classes: string) {
Assert(classes.indexOf(type) > -1, "Class '" + type + "' not found in notification element.");
return driver.sleep(500);
});
}

View File

@ -1,20 +0,0 @@
import Speakeasy = require("speakeasy");
import SeleniumWebdriver = require("selenium-webdriver");
import ClickOnButton from "./click-on-button";
export default function(driver: any, secret: string) {
const token = Speakeasy.totp({
secret: secret,
encoding: "base32"
});
return driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.id("token")), 5000)
.then(function () {
return driver.findElement(SeleniumWebdriver.By.id("token"))
.sendKeys(token);
})
.then(function () {
return ClickOnButton(driver, "Sign in");
});
}

View File

@ -1,8 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, url: string, timeout: number = 5000) {
return driver.get(url)
.then(function () {
return driver.wait(SeleniumWebdriver.until.urlIs(url), timeout);
});
}

View File

@ -1,5 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function(driver: any, url: string, timeout: number = 5000) {
return driver.wait(SeleniumWebdriver.until.urlIs(url), timeout);
}

View File

@ -1,13 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
export default function() {
before(function() {
this.driver = new SeleniumWebdriver.Builder()
.forBrowser("chrome")
.build();
})
after(function() {
this.driver.quit();
});
}

View File

@ -1,10 +1,10 @@
import Bluebird = require("bluebird");
import LoginAndRegisterTotp from "../helpers/login-and-register-totp";
import VisitPage from "../helpers/visit-page";
import FillLoginPageWithUserAndPasswordAndClick from "../helpers/fill-login-page-and-click";
import WithDriver from "../helpers/with-driver";
import ValidateTotp from "../helpers/validate-totp";
import WaitRedirected from "../helpers/wait-redirected";
import LoginAndRegisterTotp from "../helpers/LoginAndRegisterTotp";
import VisitPage from "../helpers/VisitPage";
import FillLoginPageWithUserAndPasswordAndClick from "../helpers/FillLoginPageAndClick";
import WithDriver from "../helpers/context/WithDriver";
import ValidateTotp from "../helpers/ValidateTotp";
import WaitRedirected from "../helpers/WaitRedirected";
describe("Keep me logged in", function() {
this.timeout(15000);

View File

@ -1,15 +0,0 @@
require("chromedriver");
import ChildProcess = require('child_process');
import Bluebird = require("bluebird");
const execAsync = Bluebird.promisify(ChildProcess.exec);
before(function() {
this.timeout(1000);
return execAsync("cp users_database.yml users_database.test.yml");
});
after(function() {
this.timeout(1000);
return execAsync("rm users_database.test.yml");
});

View File

@ -1,30 +0,0 @@
import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import VisitPage from '../helpers/visit-page';
import SeeNotification from '../helpers/see-notification';
import {AUTHENTICATION_FAILED} from '../../shared/UserMessages';
/**
* When user provides bad password,
* Then he gets a notification message.
*/
describe("Provide bad password", function() {
WithDriver();
describe('failed login as john', function() {
before(function() {
this.timeout(10000);
const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/")
.then(function() {
return FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'bad_password');
});
});
it('should get a notification message', function() {
this.timeout(10000);
return SeeNotification(this.driver, "error", AUTHENTICATION_FAILED);
});
});
});

View File

@ -1,39 +0,0 @@
require("chromedriver");
import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import VisitPage from '../helpers/visit-page';
import ValidateTotp from '../helpers/validate-totp';
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
import seeNotification from "../helpers/see-notification";
import {AUTHENTICATION_TOTP_FAILED} from '../../shared/UserMessages';
/**
* Given john has registered a TOTP secret,
* When he fails the TOTP challenge,
* Then he gets a notification message.
*/
describe('Fail TOTP challenge', function() {
this.timeout(10000);
WithDriver();
describe('successfully login as john', function() {
before(function() {
return LoginAndRegisterTotp(this.driver, "john", true);
});
describe('fail second factor', function() {
before(function() {
const BAD_TOKEN = "125478";
const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html")
.then(() => FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password'))
.then(() => ValidateTotp(driver, BAD_TOKEN));
});
it("get a notification message", function() {
return seeNotification(this.driver, "error", AUTHENTICATION_TOTP_FAILED);
});
});
});
});

View File

@ -0,0 +1,23 @@
import ChildProcess from 'child_process';
import Bluebird from "bluebird";
import AutheliaSuite from "../helpers/context/AutheliaSuite";
import BadPassword from "./scenarii/BadPassword";
import RegisterTotp from './scenarii/RegisterTotp';
import ResetPassword from './scenarii/ResetPassword';
import TOTPValidation from './scenarii/TOTPValidation';
const execAsync = Bluebird.promisify(ChildProcess.exec);
AutheliaSuite('Minimal configuration', function() {
this.timeout(10000);
beforeEach(function() {
return execAsync("cp users_database.example.yml users_database.yml");
});
describe('Bad password', BadPassword);
describe('Reset password', ResetPassword);
describe('TOTP Registration', RegisterTotp);
describe('TOTP Validation', TOTPValidation);
});

View File

@ -1,32 +0,0 @@
import SeleniumWebdriver = require("selenium-webdriver");
import WithDriver from '../helpers/with-driver';
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
/**
* Given the user logs in as john,
* When he register a TOTP token,
* Then he reach a page containing the secret as string an qrcode
*/
describe('Registering TOTP', function() {
this.timeout(10000);
WithDriver();
describe('successfully login as john', function() {
before('register successfully', function() {
this.timeout(10000);
return LoginAndRegisterTotp(this.driver, "john", true);
})
it("should see generated qrcode", function() {
this.driver.findElement(
SeleniumWebdriver.By.id("qrcode"),
5000);
});
it("should see generated secret", function() {
this.driver.findElement(
SeleniumWebdriver.By.id("secret"),
5000);
});
});
});

View File

@ -1,42 +0,0 @@
require("chromedriver");
import Bluebird = require("bluebird");
import ChildProcess = require("child_process");
import WithDriver from '../helpers/with-driver';
import VisitPage from '../helpers/visit-page';
import ClickOnLink from '../helpers/click-on-link';
import ClickOnButton from '../helpers/click-on-button';
import WaitRedirect from '../helpers/wait-redirected';
import FillField from "../helpers/fill-field";
import {GetLinkFromEmail} from "../helpers/get-identity-link";
import FillLoginPageAndClick from "../helpers/fill-login-page-and-click";
const execAsync = Bluebird.promisify(ChildProcess.exec);
describe('Reset password', function() {
this.timeout(10000);
WithDriver();
after(() => {
return execAsync("cp users_database.yml users_database.test.yml");
})
describe('click on reset password', function() {
it("should reset password for john", function() {
return VisitPage(this.driver, "https://login.example.com:8080/")
.then(() => ClickOnLink(this.driver, "Forgot password\?"))
.then(() => WaitRedirect(this.driver, "https://login.example.com:8080/password-reset/request"))
.then(() => FillField(this.driver, "username", "john"))
.then(() => ClickOnButton(this.driver, "Reset Password"))
.then(() => this.driver.sleep(1000)) // Simulate the time to read it from mailbox.
.then(() => GetLinkFromEmail())
.then((link) => VisitPage(this.driver, link))
.then(() => FillField(this.driver, "password1", "newpass"))
.then(() => FillField(this.driver, "password2", "newpass"))
.then(() => ClickOnButton(this.driver, "Reset Password"))
.then(() => WaitRedirect(this.driver, "https://login.example.com:8080/"))
.then(() => FillLoginPageAndClick(this.driver, "john", "newpass"))
.then(() => WaitRedirect(this.driver, "https://login.example.com:8080/secondfactor"))
});
});
});

View File

@ -0,0 +1,23 @@
import FillLoginPageWithUserAndPasswordAndClick from '../../helpers/FillLoginPageAndClick';
import VisitPage from '../../helpers/VisitPage';
import SeeNotification from '../../helpers/SeeNotification';
import {AUTHENTICATION_FAILED} from '../../../shared/UserMessages';
export default function() {
/**
* When user provides bad password,
* Then he gets a notification message.
*/
describe('failed login as john in first factor', function() {
beforeEach(async function() {
this.timeout(10000);
await VisitPage(this.driver, "https://login.example.com:8080/")
await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'bad_password');
});
it('should get a notification message', async function () {
this.timeout(10000);
await SeeNotification(this.driver, "error", AUTHENTICATION_FAILED);
});
});
}

View File

@ -0,0 +1,30 @@
import SeleniumWebdriver from "selenium-webdriver";
import LoginAndRegisterTotp from '../../helpers/LoginAndRegisterTotp';
/**
* Given the user logs in as john,
* When he register a TOTP token,
* Then he reach a page containing the secret as string an qrcode
*/
export default function() {
describe('successfully login as john', function() {
beforeEach('register successfully', async function() {
this.timeout(10000);
await LoginAndRegisterTotp(this.driver, "john", true);
})
it("should see generated qrcode", async function() {
await this.driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.className("qrcode")),
5000);
});
it("should see generated secret", async function() {
await this.driver.wait(
SeleniumWebdriver.until.elementLocated(
SeleniumWebdriver.By.className("base32-secret")),
5000);
});
});
};

View File

@ -0,0 +1,29 @@
import VisitPage from '../../helpers/VisitPage';
import ClickOnLink from '../../helpers/ClickOnLink';
import ClickOn from '../../helpers/ClickOn';
import WaitRedirected from '../../helpers/WaitRedirected';
import FillField from "../../helpers/FillField";
import {GetLinkFromEmail} from "../../helpers/GetIdentityLink";
import FillLoginPageAndClick from "../../helpers/FillLoginPageAndClick";
import SeleniumWebDriver from 'selenium-webdriver';
import IsSecondFactorStage from "../../helpers/IsSecondFactorStage";
export default function() {
it("should reset password for john", async function() {
await VisitPage(this.driver, "https://login.example.com:8080/");
await ClickOnLink(this.driver, "Forgot password\?");
await WaitRedirected(this.driver, "https://login.example.com:8080/forgot-password");
await FillField(this.driver, "username", "john");
await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button'));
await this.driver.sleep(500); // Simulate the time it takes to receive the e-mail.
const link = await GetLinkFromEmail();
await VisitPage(this.driver, link);
await FillField(this.driver, "password1", "newpass");
await FillField(this.driver, "password2", "newpass");
await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button'));
await WaitRedirected(this.driver, "https://login.example.com:8080/");
await FillLoginPageAndClick(this.driver, "john", "newpass");
await IsSecondFactorStage(this.driver);
});
}

View File

@ -0,0 +1,51 @@
import FillLoginPageWithUserAndPasswordAndClick from '../../helpers/FillLoginPageAndClick';
import WaitRedirected from '../../helpers/WaitRedirected';
import VisitPage from '../../helpers/VisitPage';
import ValidateTotp from '../../helpers/ValidateTotp';
import AccessSecret from "../../helpers/AccessSecret";
import LoginAndRegisterTotp from '../../helpers/LoginAndRegisterTotp';
import SeeNotification from '../../helpers/SeeNotification';
import { AUTHENTICATION_TOTP_FAILED } from '../../../shared/UserMessages';
export default function() {
/**
* Given john has registered a TOTP secret,
* When he validates the TOTP second factor,
* Then he has access to secret page.
*/
describe('Successfully pass second factor with TOTP', function() {
beforeEach(async function() {
const secret = await LoginAndRegisterTotp(this.driver, "john", true);
if (!secret) throw new Error('No secret!');
await VisitPage(this.driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'password');
await ValidateTotp(this.driver, secret);
await WaitRedirected(this.driver, "https://admin.example.com:8080/secret.html");
});
it("should access the secret", async function() {
await AccessSecret(this.driver);
});
});
/**
* Given john has registered a TOTP secret,
* When he fails the TOTP challenge,
* Then he gets a notification message.
*/
describe('Fail validation of second factor with TOTP', function() {
beforeEach(async function() {
await LoginAndRegisterTotp(this.driver, "john", true);
const BAD_TOKEN = "125478";
await VisitPage(this.driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
await FillLoginPageWithUserAndPasswordAndClick(this.driver, 'john', 'password');
await ValidateTotp(this.driver, BAD_TOKEN);
});
it("get a notification message", async function() {
await SeeNotification(this.driver, "error", AUTHENTICATION_TOTP_FAILED);
});
});
}

View File

@ -1,46 +0,0 @@
require("chromedriver");
import Bluebird = require("bluebird");
import WithDriver from '../helpers/with-driver';
import FillLoginPageWithUserAndPasswordAndClick from '../helpers/fill-login-page-and-click';
import WaitRedirected from '../helpers/wait-redirected';
import VisitPage from '../helpers/visit-page';
import ValidateTotp from '../helpers/validate-totp';
import AccessSecret from "../helpers/access-secret";
import LoginAndRegisterTotp from '../helpers/login-and-register-totp';
/**
* Given john has registered a TOTP secret,
* When he validates the TOTP second factor,
* Then he has access to secret page.
*/
describe('Validate TOTP factor', function() {
this.timeout(10000);
WithDriver();
describe('successfully login as john', function() {
before(function() {
const that = this;
return LoginAndRegisterTotp(this.driver, "john", true)
.then(function(secret: string) {
that.secret = secret;
})
});
describe('validate second factor', function() {
before(function() {
const secret = this.secret;
if(!secret) return Bluebird.reject(new Error("No secret!"));
const driver = this.driver;
return VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html")
.then(() => FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password'))
.then(() => ValidateTotp(driver, secret))
.then(() => WaitRedirected(driver, "https://admin.example.com:8080/secret.html"));
});
it("should access the secret", function() {
return AccessSecret(this.driver);
});
});
});
});

19
test/tsconfig.json 100644
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es6",
"allowJs": true,
"skipLibCheck": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"lib": [
"esnext"
]
}
}

9
test/types/mocha.d.ts vendored 100644
View File

@ -0,0 +1,9 @@
import { WebDriver } from "selenium-webdriver";
/*
declare module 'mocha' {
interface ISuiteCallbackContext {
driver1: WebDriver;
}
}
*/

View File

@ -0,0 +1,29 @@
###############################################################
# Users Database #
###############################################################
# This file can be used if you do not have an LDAP set up.
# List of users
users:
john:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: john.doe@authelia.com
groups:
- admins
- dev
harry:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
emails: harry.potter@authelia.com
groups: []
bob:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: bob.dylan@authelia.com
groups:
- dev
james:
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
email: james.dean@authelia.com