Migrate some tests to mocha.
parent
c5af4498ab
commit
efceb66ffa
|
@ -20,6 +20,10 @@
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.otpauthContainer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ class OneTimePasswordRegistrationView extends Component<Props> {
|
||||||
<div className={classnames(styles.qrcodeContainer, 'qrcode')}>
|
<div className={classnames(styles.qrcodeContainer, 'qrcode')}>
|
||||||
<QRCode value={secret.otpauth_url} size={180} level="Q"></QRCode>
|
<QRCode value={secret.otpauth_url} size={180} level="Q"></QRCode>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={classnames(styles.otpauthContainer, 'otpauth-secret')}>{secret.otpauth_url}</div>
|
||||||
<div className={classnames(styles.base32Container, 'base32-secret')}>{secret.base32_secret}</div>
|
<div className={classnames(styles.base32Container, 'base32-secret')}>{secret.base32_secret}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.loginButtonContainer}>
|
<div className={styles.loginButtonContainer}>
|
||||||
|
|
|
@ -358,6 +358,15 @@
|
||||||
"integrity": "sha512-J7nx6JzxmtT4zyvfLipYL7jNaxvlCWpyG7JhhCQ4fQyG+AGfovAHoYR55TFx+X8akfkUJYpt5JG6GPeFMjZaCQ==",
|
"integrity": "sha512-J7nx6JzxmtT4zyvfLipYL7jNaxvlCWpyG7JhhCQ4fQyG+AGfovAHoYR55TFx+X8akfkUJYpt5JG6GPeFMjZaCQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/node-fetch": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-tR1ekaXUGpmzOcDXWU9BW73YfA2/VW1DF1FH+wlJ82BbCSnWTbdX+JkqWQXWKIGsFPnPsYadbXfNgz28g+ccWg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "10.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/nodemailer": {
|
"@types/nodemailer": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-4.6.0.tgz",
|
||||||
|
@ -5251,6 +5260,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nocache/-/nocache-2.0.0.tgz",
|
||||||
"integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA="
|
"integrity": "sha1-ICtIAhoMTL3i34DeFaF0Q8i0OYA="
|
||||||
},
|
},
|
||||||
|
"node-fetch": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"nodemailer": {
|
"nodemailer": {
|
||||||
"version": "4.6.4",
|
"version": "4.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.4.tgz",
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"@types/mockdate": "^2.0.0",
|
"@types/mockdate": "^2.0.0",
|
||||||
"@types/mongodb": "^3.0.9",
|
"@types/mongodb": "^3.0.9",
|
||||||
"@types/nedb": "^1.8.3",
|
"@types/nedb": "^1.8.3",
|
||||||
|
"@types/node-fetch": "^2.1.4",
|
||||||
"@types/nodemailer": "^4.6.0",
|
"@types/nodemailer": "^4.6.0",
|
||||||
"@types/nodemailer-direct-transport": "^1.0.31",
|
"@types/nodemailer-direct-transport": "^1.0.31",
|
||||||
"@types/nodemailer-smtp-transport": "^2.7.4",
|
"@types/nodemailer-smtp-transport": "^2.7.4",
|
||||||
|
@ -98,6 +99,7 @@
|
||||||
"jquery": "^3.2.1",
|
"jquery": "^3.2.1",
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
"mockdate": "^2.0.1",
|
"mockdate": "^2.0.1",
|
||||||
|
"node-fetch": "^2.3.0",
|
||||||
"nodemon": "^1.18.9",
|
"nodemon": "^1.18.9",
|
||||||
"nyc": "^13.1.0",
|
"nyc": "^13.1.0",
|
||||||
"query-string": "^6.0.0",
|
"query-string": "^6.0.0",
|
||||||
|
|
|
@ -24,8 +24,8 @@ var tsWatcher = chokidar.watch(['server', 'shared/**/*.ts', 'node_modules'], {
|
||||||
|
|
||||||
// Properly cleanup server and client if ctrl-c is hit
|
// Properly cleanup server and client if ctrl-c is hit
|
||||||
process.on('SIGINT', function() {
|
process.on('SIGINT', function() {
|
||||||
killServer();
|
killServer(() => {});
|
||||||
killClient();
|
killClient(() => {});
|
||||||
fs.unlinkSync(ENVIRONMENT_FILENAME);
|
fs.unlinkSync(ENVIRONMENT_FILENAME);
|
||||||
process.exit();
|
process.exit();
|
||||||
});
|
});
|
||||||
|
@ -60,21 +60,31 @@ function startClient() {
|
||||||
|
|
||||||
function killServer(onExit) {
|
function killServer(onExit) {
|
||||||
if (serverProcess) {
|
if (serverProcess) {
|
||||||
process.kill(-serverProcess.pid);
|
|
||||||
serverProcess.on('exit', () => {
|
serverProcess.on('exit', () => {
|
||||||
serverProcess = undefined;
|
serverProcess = undefined;
|
||||||
onExit();
|
onExit();
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
process.kill(-serverProcess.pid);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
onExit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function killClient(onExit) {
|
function killClient(onExit) {
|
||||||
if (clientProcess) {
|
if (clientProcess) {
|
||||||
process.kill(-clientProcess.pid);
|
|
||||||
clientProcess.on('exit', () => {
|
clientProcess.on('exit', () => {
|
||||||
clientProcess = undefined;
|
clientProcess = undefined;
|
||||||
onExit();
|
onExit();
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
process.kill(-clientProcess.pid);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
onExit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,9 +42,9 @@ export function register(app: Express.Application,
|
||||||
vars: ServerVariables) {
|
vars: ServerVariables) {
|
||||||
|
|
||||||
app.post(pre_validation_endpoint,
|
app.post(pre_validation_endpoint,
|
||||||
get_start_validation(handler, post_validation_endpoint, vars));
|
post_start_validation(handler, vars));
|
||||||
app.post(post_validation_endpoint,
|
app.post(post_validation_endpoint,
|
||||||
get_finish_validation(handler, vars));
|
post_finish_validation(handler, vars));
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkIdentityToken(req: Express.Request, identityToken: string)
|
function checkIdentityToken(req: Express.Request, identityToken: string)
|
||||||
|
@ -55,7 +55,7 @@ function checkIdentityToken(req: Express.Request, identityToken: string)
|
||||||
return BluebirdPromise.resolve();
|
return BluebirdPromise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get_finish_validation(handler: IdentityValidable,
|
export function post_finish_validation(handler: IdentityValidable,
|
||||||
vars: ServerVariables)
|
vars: ServerVariables)
|
||||||
: Express.RequestHandler {
|
: Express.RequestHandler {
|
||||||
|
|
||||||
|
@ -88,8 +88,7 @@ export function get_finish_validation(handler: IdentityValidable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get_start_validation(handler: IdentityValidable,
|
export function post_start_validation(handler: IdentityValidable,
|
||||||
postValidationEndpoint: string,
|
|
||||||
vars: ServerVariables)
|
vars: ServerVariables)
|
||||||
: Express.RequestHandler {
|
: Express.RequestHandler {
|
||||||
return function (req: Express.Request, res: Express.Response)
|
return function (req: Express.Request, res: Express.Response)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
import { UserDataStore } from "../../../../storage/UserDataStore";
|
|
||||||
import objectPath = require("object-path");
|
import objectPath = require("object-path");
|
||||||
import u2f_common = require("../U2FCommon");
|
import u2f_common = require("../U2FCommon");
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
|
|
|
@ -3,11 +3,8 @@ import objectPath = require("object-path");
|
||||||
import u2f_common = require("../U2FCommon");
|
import u2f_common = require("../U2FCommon");
|
||||||
import BluebirdPromise = require("bluebird");
|
import BluebirdPromise = require("bluebird");
|
||||||
import express = require("express");
|
import express = require("express");
|
||||||
import { UserDataStore } from "../../../../storage/UserDataStore";
|
|
||||||
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
|
import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument";
|
||||||
import { Winston } from "../../../../../../types/Dependencies";
|
|
||||||
import U2f = require("u2f");
|
import U2f = require("u2f");
|
||||||
import exceptions = require("../../../../Exceptions");
|
|
||||||
import redirect from "../../redirect";
|
import redirect from "../../redirect";
|
||||||
import ErrorReplies = require("../../../../ErrorReplies");
|
import ErrorReplies = require("../../../../ErrorReplies");
|
||||||
import { ServerVariables } from "../../../../ServerVariables";
|
import { ServerVariables } from "../../../../ServerVariables";
|
||||||
|
|
|
@ -78,6 +78,8 @@ export default function (vars: ServerVariables) {
|
||||||
ErrorReplies.replyWithError401(req, res, vars.logger))
|
ErrorReplies.replyWithError401(req, res, vars.logger))
|
||||||
// The user is not yet authenticated -> 401
|
// The user is not yet authenticated -> 401
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
// This redirect parameter is used in Kubernetes to annotate the ingress with
|
||||||
|
// the url to the authentication portal.
|
||||||
const redirectUrl = getRedirectParam(req);
|
const redirectUrl = getRedirectParam(req);
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err);
|
ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err);
|
||||||
|
|
|
@ -30,15 +30,15 @@ function setupTotp(app: Express.Application, vars: ServerVariables) {
|
||||||
RequireValidatedFirstFactor.middleware(vars.logger),
|
RequireValidatedFirstFactor.middleware(vars.logger),
|
||||||
TOTPSignGet.default(vars));
|
TOTPSignGet.default(vars));
|
||||||
|
|
||||||
app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
|
app.post(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_POST,
|
||||||
RequireValidatedFirstFactor.middleware(vars.logger));
|
RequireValidatedFirstFactor.middleware(vars.logger));
|
||||||
|
|
||||||
app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET,
|
app.post(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_POST,
|
||||||
RequireValidatedFirstFactor.middleware(vars.logger));
|
RequireValidatedFirstFactor.middleware(vars.logger));
|
||||||
|
|
||||||
IdentityCheckMiddleware.register(app,
|
IdentityCheckMiddleware.register(app,
|
||||||
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
|
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_POST,
|
||||||
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET,
|
Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_POST,
|
||||||
new TOTPRegistrationIdentityHandler(vars.logger,
|
new TOTPRegistrationIdentityHandler(vars.logger,
|
||||||
vars.userDataStore, vars.totpHandler, vars.config.totp),
|
vars.userDataStore, vars.totpHandler, vars.config.totp),
|
||||||
vars);
|
vars);
|
||||||
|
|
|
@ -143,7 +143,7 @@ export const SECOND_FACTOR_U2F_IDENTITY_FINISH_POST = "/api/secondfactor/u2f/ide
|
||||||
*
|
*
|
||||||
* @apiDescription Initiates the identity validation
|
* @apiDescription Initiates the identity validation
|
||||||
*/
|
*/
|
||||||
export const SECOND_FACTOR_TOTP_IDENTITY_START_GET = "/api/secondfactor/totp/identity/start";
|
export const SECOND_FACTOR_TOTP_IDENTITY_START_POST = "/api/secondfactor/totp/identity/start";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -159,7 +159,7 @@ export const SECOND_FACTOR_TOTP_IDENTITY_START_GET = "/api/secondfactor/totp/ide
|
||||||
* @apiDescription Serves the TOTP registration page that displays the secret.
|
* @apiDescription Serves the TOTP registration page that displays the secret.
|
||||||
* The secret is a QRCode and a base32 secret.
|
* The secret is a QRCode and a base32 secret.
|
||||||
*/
|
*/
|
||||||
export const SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET = "/api/secondfactor/totp/identity/finish";
|
export const SECOND_FACTOR_TOTP_IDENTITY_FINISH_POST = "/api/secondfactor/totp/identity/finish";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
Feature: User has access restricted access to domains
|
|
||||||
|
|
||||||
@need-registered-user-john
|
|
||||||
Scenario: User john has admin access
|
|
||||||
When I visit "https://login.example.com:8080?rd=https://home.example.com:8080/"
|
|
||||||
And I login with user "john" and password "password"
|
|
||||||
And I use "REGISTERED" as TOTP token handle
|
|
||||||
And I click on "Sign in"
|
|
||||||
And I'm redirected to "https://home.example.com:8080/"
|
|
||||||
Then I have access to "https://public.example.com:8080/secret.html"
|
|
||||||
And I have access to "https://dev.example.com:8080/groups/admin/secret.html"
|
|
||||||
And I have access to "https://dev.example.com:8080/groups/dev/secret.html"
|
|
||||||
And I have access to "https://dev.example.com:8080/users/john/secret.html"
|
|
||||||
And I have access to "https://dev.example.com:8080/users/harry/secret.html"
|
|
||||||
And I have access to "https://dev.example.com:8080/users/bob/secret.html"
|
|
||||||
And I have access to "https://admin.example.com:8080/secret.html"
|
|
||||||
And I have access to "https://mx1.mail.example.com:8080/secret.html"
|
|
||||||
And I have access to "https://single_factor.example.com:8080/secret.html"
|
|
||||||
And I have no access to "https://mx2.mail.example.com:8080/secret.html"
|
|
||||||
|
|
||||||
@need-registered-user-bob
|
|
||||||
Scenario: User bob has restricted access
|
|
||||||
When I visit "https://login.example.com:8080?rd=https://home.example.com:8080/"
|
|
||||||
And I login with user "bob" and password "password"
|
|
||||||
And I use "REGISTERED" as TOTP token handle
|
|
||||||
And I click on "Sign in"
|
|
||||||
And I'm redirected to "https://home.example.com:8080/"
|
|
||||||
Then I have access to "https://public.example.com:8080/secret.html"
|
|
||||||
And I have no access to "https://dev.example.com:8080/groups/admin/secret.html"
|
|
||||||
And I have access to "https://dev.example.com:8080/groups/dev/secret.html"
|
|
||||||
And I have no access to "https://dev.example.com:8080/users/john/secret.html"
|
|
||||||
And I have no access to "https://dev.example.com:8080/users/harry/secret.html"
|
|
||||||
And I have access to "https://dev.example.com:8080/users/bob/secret.html"
|
|
||||||
And I have no access to "https://admin.example.com:8080/secret.html"
|
|
||||||
And I have access to "https://mx1.mail.example.com:8080/secret.html"
|
|
||||||
And I have access to "https://single_factor.example.com:8080/secret.html"
|
|
||||||
And I have access to "https://mx2.mail.example.com:8080/secret.html"
|
|
||||||
|
|
||||||
@need-registered-user-harry
|
|
||||||
Scenario: User harry has restricted access
|
|
||||||
When I visit "https://login.example.com:8080?rd=https://home.example.com:8080/"
|
|
||||||
And I login with user "harry" and password "password"
|
|
||||||
And I use "REGISTERED" as TOTP token handle
|
|
||||||
And I click on "Sign in"
|
|
||||||
And I'm redirected to "https://home.example.com:8080/"
|
|
||||||
Then I have access to "https://public.example.com:8080/secret.html"
|
|
||||||
And I have no access to "https://dev.example.com:8080/groups/admin/secret.html"
|
|
||||||
And I have no access to "https://dev.example.com:8080/groups/dev/secret.html"
|
|
||||||
And I have no access to "https://dev.example.com:8080/users/john/secret.html"
|
|
||||||
And I have access to "https://dev.example.com:8080/users/harry/secret.html"
|
|
||||||
And I have no access to "https://dev.example.com:8080/users/bob/secret.html"
|
|
||||||
And I have no access to "https://admin.example.com:8080/secret.html"
|
|
||||||
And I have no access to "https://mx1.mail.example.com:8080/secret.html"
|
|
||||||
And I have access to "https://single_factor.example.com:8080/secret.html"
|
|
||||||
And I have no access to "https://mx2.mail.example.com:8080/secret.html"
|
|
|
@ -1,9 +0,0 @@
|
||||||
Feature: Generic tests on Authelia endpoints
|
|
||||||
|
|
||||||
Scenario: /api/verify replies with error when redirect parameter is not provided
|
|
||||||
When I query "https://authelia.example.com:8080/api/verify"
|
|
||||||
Then I get error code 401
|
|
||||||
|
|
||||||
Scenario: /api/verify redirects when redirect parameter is provided
|
|
||||||
When I query "https://authelia.example.com:8080/api/verify?rd=http://login.example.com:8080"
|
|
||||||
Then I get redirected to "http://login.example.com:8080"
|
|
|
@ -1,38 +1,5 @@
|
||||||
Feature: Authentication scenarii
|
Feature: Authentication scenarii
|
||||||
|
|
||||||
Scenario: User succeeds first factor
|
|
||||||
Given I visit "https://login.example.com:8080/"
|
|
||||||
When I set field "username" to "bob"
|
|
||||||
And I set field "password" to "password"
|
|
||||||
And I click on "Sign in"
|
|
||||||
Then I'm redirected to "https://login.example.com:8080/secondfactor"
|
|
||||||
|
|
||||||
Scenario: User fails first factor
|
|
||||||
Given I visit "https://login.example.com:8080/"
|
|
||||||
When I set field "username" to "john"
|
|
||||||
And I set field "password" to "bad-password"
|
|
||||||
And I click on "Sign in"
|
|
||||||
Then I get a notification of type "error" with message "Authentication failed. Please check your credentials."
|
|
||||||
|
|
||||||
Scenario: User registers TOTP secret and succeeds authentication
|
|
||||||
Given I visit "https://login.example.com:8080/"
|
|
||||||
And I login with user "john" and password "password"
|
|
||||||
And I register a TOTP secret called "Sec0"
|
|
||||||
When I visit "https://admin.example.com:8080/secret.html"
|
|
||||||
And I'm redirected to "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html"
|
|
||||||
And I login with user "john" and password "password"
|
|
||||||
And I use "Sec0" as TOTP token handle
|
|
||||||
And I click on "Sign in"
|
|
||||||
Then I'm redirected to "https://admin.example.com:8080/secret.html"
|
|
||||||
|
|
||||||
Scenario: User fails TOTP second factor
|
|
||||||
When I visit "https://admin.example.com:8080/secret.html"
|
|
||||||
And I'm redirected to "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html"
|
|
||||||
And I login with user "john" and password "password"
|
|
||||||
And I use "BADTOKEN" as TOTP token
|
|
||||||
And I click on "Sign in"
|
|
||||||
Then I get a notification of type "error" with message "Authentication failed. Have you already registered your secret?"
|
|
||||||
|
|
||||||
Scenario: Logout redirects user to redirect URL given in parameter
|
Scenario: Logout redirects user to redirect URL given in parameter
|
||||||
When I visit "https://login.example.com:8080/logout?rd=https://home.example.com:8080/"
|
When I visit "https://login.example.com:8080/logout?rd=https://home.example.com:8080/"
|
||||||
Then I'm redirected to "https://home.example.com:8080/"
|
Then I'm redirected to "https://home.example.com:8080/"
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
Feature: Register secret for second factor
|
|
||||||
|
|
||||||
Scenario: Register a TOTP secret with correct label and issuer
|
|
||||||
Given I visit "https://login.example.com:8080/"
|
|
||||||
And I login with user "john" and password "password"
|
|
||||||
When I register a TOTP secret called "Sec0"
|
|
||||||
Then the otpauth url has label "john" and issuer "authelia.com"
|
|
||||||
|
|
||||||
@needs-totp_issuer-config
|
|
||||||
Scenario: Register a TOTP secret with correct label and custom issuer
|
|
||||||
Given I visit "https://login.example.com:8080/"
|
|
||||||
And I login with user "john" and password "password"
|
|
||||||
When I register a TOTP secret called "Sec0"
|
|
||||||
Then the otpauth url has label "john" and issuer "custom.com"
|
|
|
@ -1,39 +0,0 @@
|
||||||
Feature: User is able to reset his password
|
|
||||||
|
|
||||||
Scenario: User is redirected to password reset page
|
|
||||||
Given I'm on "https://login.example.com:8080"
|
|
||||||
When I click on the link "Forgot password?"
|
|
||||||
Then I'm redirected to "https://login.example.com:8080/password-reset/request"
|
|
||||||
|
|
||||||
Scenario: User get an email with a link to reset password
|
|
||||||
Given I'm on "https://login.example.com:8080/password-reset/request"
|
|
||||||
When I set field "username" to "james"
|
|
||||||
And I click on "Reset Password"
|
|
||||||
Then I get a notification of type "success" with message "An email has been sent to you. Follow the link to change your password."
|
|
||||||
|
|
||||||
Scenario: Request password for unexisting user should behave like existing user
|
|
||||||
Given I'm on "https://login.example.com:8080/password-reset/request"
|
|
||||||
When I set field "username" to "fake_user"
|
|
||||||
And I click on "Reset Password"
|
|
||||||
Then I get a notification of type "success" with message "An email has been sent to you. Follow the link to change your password."
|
|
||||||
|
|
||||||
Scenario: User resets his password
|
|
||||||
Given I'm on "https://login.example.com:8080/password-reset/request"
|
|
||||||
And I set field "username" to "james"
|
|
||||||
And I click on "Reset Password"
|
|
||||||
When I click on the link of the email
|
|
||||||
And I set field "password1" to "newpassword"
|
|
||||||
And I set field "password2" to "newpassword"
|
|
||||||
And I click on "Reset Password"
|
|
||||||
Then I'm redirected to "https://login.example.com:8080/"
|
|
||||||
|
|
||||||
|
|
||||||
Scenario: User does not confirm new password
|
|
||||||
Given I'm on "https://login.example.com:8080/password-reset/request"
|
|
||||||
And I set field "username" to "james"
|
|
||||||
And I click on "Reset Password"
|
|
||||||
When I click on the link of the email
|
|
||||||
And I set field "password1" to "newpassword"
|
|
||||||
And I set field "password2" to "newpassword2"
|
|
||||||
And I click on "Reset Password"
|
|
||||||
Then I get a notification of type "warning" with message "The passwords are different."
|
|
|
@ -1,16 +0,0 @@
|
||||||
Feature: Non authenticated users have no access to certain pages
|
|
||||||
|
|
||||||
Scenario: Anonymous user has no access to protected pages
|
|
||||||
Then I get the following status code when requesting:
|
|
||||||
| url | code | method |
|
|
||||||
| https://login.example.com:8080/secondfactor | 401 | GET |
|
|
||||||
| https://login.example.com:8080/secondfactor/u2f/identity/start | 401 | GET |
|
|
||||||
| https://login.example.com:8080/secondfactor/u2f/identity/finish | 401 | GET |
|
|
||||||
| https://login.example.com:8080/secondfactor/totp/identity/start | 401 | GET |
|
|
||||||
| https://login.example.com:8080/secondfactor/totp/identity/finish | 401 | GET |
|
|
||||||
| https://login.example.com:8080/loggedin | 401 | GET |
|
|
||||||
| https://login.example.com:8080/api/totp | 401 | POST |
|
|
||||||
| https://login.example.com:8080/api/u2f/sign_request | 401 | GET |
|
|
||||||
| https://login.example.com:8080/api/u2f/sign | 401 | POST |
|
|
||||||
| https://login.example.com:8080/api/u2f/register_request | 401 | GET |
|
|
||||||
| https://login.example.com:8080/api/u2f/register | 401 | POST |
|
|
|
@ -1,20 +0,0 @@
|
||||||
@needs-inactivity-config
|
|
||||||
Feature: Session is closed after a certain amount of time
|
|
||||||
|
|
||||||
@need-authenticated-user-john
|
|
||||||
Scenario: An authenticated user is disconnected after a certain inactivity period
|
|
||||||
Given I have access to "https://public.example.com:8080/secret.html"
|
|
||||||
When I sleep for 6 seconds
|
|
||||||
And I visit "https://public.example.com:8080/secret.html"
|
|
||||||
Then I'm redirected to "https://login.example.com:8080/?rd=https://public.example.com:8080/secret.html"
|
|
||||||
|
|
||||||
@need-authenticated-user-john
|
|
||||||
Scenario: An authenticated user is disconnected after session expiration period
|
|
||||||
Given I have access to "https://public.example.com:8080/secret.html"
|
|
||||||
When I sleep for 4 seconds
|
|
||||||
And I visit "https://public.example.com:8080/secret.html"
|
|
||||||
And I sleep for 4 seconds
|
|
||||||
And I visit "https://public.example.com:8080/secret.html"
|
|
||||||
And I sleep for 4 seconds
|
|
||||||
And I visit "https://public.example.com:8080/secret.html"
|
|
||||||
Then I'm redirected to "https://login.example.com:8080/?rd=https://public.example.com:8080/secret.html"
|
|
|
@ -1,4 +1,4 @@
|
||||||
import SeleniumWebdriver, { ThenableWebDriver, WebDriver } from "selenium-webdriver";
|
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
|
||||||
import Assert = require("assert");
|
import Assert = require("assert");
|
||||||
|
|
||||||
export default async function(driver: WebDriver, type: string, message: string) {
|
export default async function(driver: WebDriver, type: string, message: string) {
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
|
||||||
|
|
||||||
|
// Verify if the current page contains "This is a very important secret!".
|
||||||
|
export default async function(driver: WebDriver, timeout: number = 5000) {
|
||||||
|
const el = await driver.wait(
|
||||||
|
SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.tagName('body')), timeout);
|
||||||
|
|
||||||
|
await driver.wait(
|
||||||
|
SeleniumWebdriver.until.elementTextContains(el, "This is a very important secret!"), timeout);
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ function AutheliaSuiteBase(description: string, configPath: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
return context('Suite: ' + description, function(this: Mocha.ISuiteCallbackContext) {
|
return context('Suite: ' + description, function(this: Mocha.ISuiteCallbackContext) {
|
||||||
WithDriver.call(this);
|
|
||||||
cb.call(this);
|
cb.call(this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,22 +2,30 @@ require("chromedriver");
|
||||||
import chrome from 'selenium-webdriver/chrome';
|
import chrome from 'selenium-webdriver/chrome';
|
||||||
import SeleniumWebdriver from "selenium-webdriver";
|
import SeleniumWebdriver from "selenium-webdriver";
|
||||||
|
|
||||||
export default function() {
|
export default function(forEach: boolean = false) {
|
||||||
let options = new chrome.Options();
|
let options = new chrome.Options();
|
||||||
|
|
||||||
if (process.env['HEADLESS'] == 'y') {
|
if (process.env['HEADLESS'] == 'y') {
|
||||||
options = options.headless();
|
options = options.headless();
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function() {
|
function beforeBlock(this: Mocha.IHookCallbackContext) {
|
||||||
const driver = new SeleniumWebdriver.Builder()
|
const driver = new SeleniumWebdriver.Builder()
|
||||||
.forBrowser("chrome")
|
.forBrowser("chrome")
|
||||||
.setChromeOptions(options)
|
.setChromeOptions(options)
|
||||||
.build();
|
.build();
|
||||||
this.driver = driver;
|
this.driver = driver;
|
||||||
});
|
}
|
||||||
|
|
||||||
afterEach(function() {
|
function afterBlock(this: Mocha.IHookCallbackContext) {
|
||||||
this.driver.quit();
|
return this.driver.quit();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (forEach) {
|
||||||
|
beforeEach(beforeBlock);
|
||||||
|
afterEach(afterBlock);
|
||||||
|
} else {
|
||||||
|
before(beforeBlock);
|
||||||
|
after(afterBlock);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import Request from 'request-promise';
|
||||||
|
import Fetch from 'node-fetch';
|
||||||
|
import Assert from 'assert';
|
||||||
|
import { StatusCodeError } from 'request-promise/errors';
|
||||||
|
|
||||||
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
|
|
||||||
|
// Sent a GET request to the url and expect a 401
|
||||||
|
export async function GET_Expect401(url: string) {
|
||||||
|
try {
|
||||||
|
await Request.get(url, {
|
||||||
|
json: true,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
});
|
||||||
|
throw new Error('No response');
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof StatusCodeError) {
|
||||||
|
Assert.equal(e.statusCode, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST_Expect401(url: string, body?: any) {
|
||||||
|
try {
|
||||||
|
await Request.post(url, {
|
||||||
|
json: true,
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
throw new Error('No response');
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof StatusCodeError) {
|
||||||
|
Assert.equal(e.statusCode, 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET_ExpectRedirect(url: string, redirectionUrl: string) {
|
||||||
|
const response = await Fetch(url, {redirect: 'manual'});
|
||||||
|
|
||||||
|
if (response.status == 302) {
|
||||||
|
const body = await response.text();
|
||||||
|
Assert.equal(body, 'Found. Redirecting to ' + redirectionUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No redirect');
|
||||||
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
import AutheliaSuite from "../../helpers/context/AutheliaSuite";
|
import AutheliaSuite from "../../helpers/context/AutheliaSuite";
|
||||||
import MongoConnectionRecovery from "./scenarii/MongoConnectionRecovery";
|
import MongoConnectionRecovery from "./scenarii/MongoConnectionRecovery";
|
||||||
import EnforceInternalRedirectionsOnly from "./scenarii/EnforceInternalRedirectionsOnly";
|
import EnforceInternalRedirectionsOnly from "./scenarii/EnforceInternalRedirectionsOnly";
|
||||||
|
import AccessControl from "./scenarii/AccessControl";
|
||||||
|
|
||||||
AutheliaSuite('Complete configuration', __dirname + '/config.yml', function() {
|
AutheliaSuite('Complete configuration', __dirname + '/config.yml', function() {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
|
|
||||||
|
describe('Access control', AccessControl);
|
||||||
|
|
||||||
describe('Mongo broken connection recovery', MongoConnectionRecovery);
|
describe('Mongo broken connection recovery', MongoConnectionRecovery);
|
||||||
describe('Enforce internal redirections only', EnforceInternalRedirectionsOnly);
|
describe('Enforce internal redirections only', EnforceInternalRedirectionsOnly);
|
||||||
});
|
});
|
|
@ -0,0 +1,107 @@
|
||||||
|
import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp";
|
||||||
|
import VisitPage from "../../../helpers/VisitPage";
|
||||||
|
import ObserveSecret from "../../../helpers/assertions/ObserveSecret";
|
||||||
|
import WithDriver from "../../../helpers/context/WithDriver";
|
||||||
|
import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick";
|
||||||
|
import ValidateTotp from "../../../helpers/ValidateTotp";
|
||||||
|
import WaitRedirected from "../../../helpers/WaitRedirected";
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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/');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// we verify that the user has only access to want he is granted to.
|
||||||
|
export default function() {
|
||||||
|
|
||||||
|
// We ensure that bob has access to what he is granted to
|
||||||
|
describe('Permissions of user john', function() {
|
||||||
|
after(async function() {
|
||||||
|
await Logout(this.driver);
|
||||||
|
})
|
||||||
|
|
||||||
|
WithDriver();
|
||||||
|
|
||||||
|
before(async function() {
|
||||||
|
const secret = await LoginAndRegisterTotp(this.driver, "john", true);
|
||||||
|
await VisitPage(this.driver, 'https://login.example.com:8080/');
|
||||||
|
await FillLoginPageAndClick(this.driver, 'john', 'password', false);
|
||||||
|
await ValidateTotp(this.driver, secret);
|
||||||
|
})
|
||||||
|
|
||||||
|
ShouldHaveAccessTo('https://public.example.com:8080/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://dev.example.com:8080/groups/admin/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://dev.example.com:8080/groups/dev/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://dev.example.com:8080/users/john/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://dev.example.com:8080/users/harry/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://dev.example.com:8080/users/bob/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://admin.example.com:8080/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://mx1.mail.example.com:8080/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://single_factor.example.com:8080/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://mx2.mail.example.com:8080/secret.html');
|
||||||
|
})
|
||||||
|
|
||||||
|
// We ensure that bob has access to what he is granted to
|
||||||
|
describe('Permissions of user bob', function() {
|
||||||
|
after(async function() {
|
||||||
|
await Logout(this.driver);
|
||||||
|
})
|
||||||
|
|
||||||
|
WithDriver();
|
||||||
|
|
||||||
|
before(async function() {
|
||||||
|
const secret = await LoginAndRegisterTotp(this.driver, "bob", true);
|
||||||
|
await VisitPage(this.driver, 'https://login.example.com:8080/');
|
||||||
|
await FillLoginPageAndClick(this.driver, 'bob', 'password', false);
|
||||||
|
await ValidateTotp(this.driver, secret);
|
||||||
|
})
|
||||||
|
|
||||||
|
ShouldHaveAccessTo('https://public.example.com:8080/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://dev.example.com:8080/groups/admin/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://dev.example.com:8080/groups/dev/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://dev.example.com:8080/users/john/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://dev.example.com:8080/users/harry/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://dev.example.com:8080/users/bob/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://admin.example.com:8080/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://mx1.mail.example.com:8080/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://single_factor.example.com:8080/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://mx2.mail.example.com:8080/secret.html');
|
||||||
|
})
|
||||||
|
|
||||||
|
// We ensure that harry has access to what he is granted to
|
||||||
|
describe('Permissions of user harry', function() {
|
||||||
|
after(async function() {
|
||||||
|
await Logout(this.driver);
|
||||||
|
})
|
||||||
|
|
||||||
|
WithDriver();
|
||||||
|
|
||||||
|
before(async function() {
|
||||||
|
const secret = await LoginAndRegisterTotp(this.driver, "harry", true);
|
||||||
|
await VisitPage(this.driver, 'https://login.example.com:8080/');
|
||||||
|
await FillLoginPageAndClick(this.driver, 'harry', 'password', false);
|
||||||
|
await ValidateTotp(this.driver, secret);
|
||||||
|
})
|
||||||
|
|
||||||
|
ShouldHaveAccessTo('https://public.example.com:8080/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://dev.example.com:8080/groups/admin/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://dev.example.com:8080/groups/dev/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://dev.example.com:8080/users/john/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://dev.example.com:8080/users/harry/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://dev.example.com:8080/users/bob/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://admin.example.com:8080/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://mx1.mail.example.com:8080/secret.html');
|
||||||
|
ShouldHaveAccessTo('https://single_factor.example.com:8080/secret.html');
|
||||||
|
ShouldNotHaveAccessTo('https://mx2.mail.example.com:8080/secret.html');
|
||||||
|
})
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import ValidateTotp from "../../../helpers/ValidateTotp";
|
||||||
import Logout from "../../../helpers/Logout";
|
import Logout from "../../../helpers/Logout";
|
||||||
import WaitRedirected from "../../../helpers/WaitRedirected";
|
import WaitRedirected from "../../../helpers/WaitRedirected";
|
||||||
import IsAlreadyAuthenticatedStage from "../../../helpers/IsAlreadyAuthenticatedStage";
|
import IsAlreadyAuthenticatedStage from "../../../helpers/IsAlreadyAuthenticatedStage";
|
||||||
|
import WithDriver from "../../../helpers/context/WithDriver";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Authelia should not be vulnerable to open redirection. Otherwise it would aid an
|
* Authelia should not be vulnerable to open redirection. Otherwise it would aid an
|
||||||
|
@ -14,6 +15,7 @@ import IsAlreadyAuthenticatedStage from "../../../helpers/IsAlreadyAuthenticated
|
||||||
* the URL is pointing to an external domain.
|
* the URL is pointing to an external domain.
|
||||||
*/
|
*/
|
||||||
export default function() {
|
export default function() {
|
||||||
|
WithDriver(true);
|
||||||
describe("Only redirection to a subdomain of the protected domain should be allowed", function() {
|
describe("Only redirection to a subdomain of the protected domain should be allowed", function() {
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
let secret: string;
|
let secret: string;
|
||||||
|
@ -44,18 +46,22 @@ export default function() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('blocked redirection', function() {
|
describe('Cannot redirect to https://www.google.fr', function() {
|
||||||
// Do not redirect to another domain than example.com
|
// Do not redirect to another domain than example.com
|
||||||
CannotRedirectTo("https://www.google.fr");
|
CannotRedirectTo("https://www.google.fr");
|
||||||
|
});
|
||||||
|
|
||||||
// Do not redirect to rogue domain
|
describe('Cannot redirect to https://public.example.com.a:8080', function() {
|
||||||
|
// Do not redirect to another domain than example.com
|
||||||
CannotRedirectTo("https://public.example.com.a:8080");
|
CannotRedirectTo("https://public.example.com.a:8080");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cannot redirect to http://public.example.com:8080', function() {
|
||||||
// Do not redirect to http website
|
// Do not redirect to http website
|
||||||
CannotRedirectTo("http://public.example.com:8080");
|
CannotRedirectTo("http://public.example.com:8080");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('allowed redirection', function() {
|
describe('Can redirect to https://public.example.com:8080/', function() {
|
||||||
// Can redirect to any subdomain of the domain protected by Authelia.
|
// Can redirect to any subdomain of the domain protected by Authelia.
|
||||||
CanRedirectTo("https://public.example.com:8080/");
|
CanRedirectTo("https://public.example.com:8080/");
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp";
|
import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp";
|
||||||
import FullLogin from "../../../helpers/FullLogin";
|
import FullLogin from "../../../helpers/FullLogin";
|
||||||
import child_process from 'child_process';
|
import child_process from 'child_process';
|
||||||
|
import WithDriver from "../../../helpers/context/WithDriver";
|
||||||
|
import Logout from "../../../helpers/Logout";
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
|
after(async function() {
|
||||||
|
await Logout(this.driver);
|
||||||
|
})
|
||||||
|
|
||||||
|
WithDriver();
|
||||||
|
|
||||||
it("should be able to login after mongo restarts", async function() {
|
it("should be able to login after mongo restarts", async function() {
|
||||||
this.timeout(30000);
|
this.timeout(30000);
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ session:
|
||||||
secret: unsecure_session_secret
|
secret: unsecure_session_secret
|
||||||
domain: example.com
|
domain: example.com
|
||||||
inactivity: 5000
|
inactivity: 5000
|
||||||
|
expiration: 8000
|
||||||
|
|
||||||
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
# Configuration of the storage backend used to store data and secrets. i.e. totp data
|
||||||
storage:
|
storage:
|
||||||
|
|
|
@ -7,6 +7,8 @@ import RegisterTotp from './scenarii/RegisterTotp';
|
||||||
import ResetPassword from './scenarii/ResetPassword';
|
import ResetPassword from './scenarii/ResetPassword';
|
||||||
import TOTPValidation from './scenarii/TOTPValidation';
|
import TOTPValidation from './scenarii/TOTPValidation';
|
||||||
import Inactivity from './scenarii/Inactivity';
|
import Inactivity from './scenarii/Inactivity';
|
||||||
|
import BackendProtection from './scenarii/BackendProtection';
|
||||||
|
import VerifyEndpoint from './scenarii/VerifyEndpoint';
|
||||||
|
|
||||||
const execAsync = Bluebird.promisify(ChildProcess.exec);
|
const execAsync = Bluebird.promisify(ChildProcess.exec);
|
||||||
|
|
||||||
|
@ -16,6 +18,9 @@ AutheliaSuite('Minimal configuration', __dirname + '/config.yml', function() {
|
||||||
return execAsync("cp users_database.example.yml users_database.yml");
|
return execAsync("cp users_database.example.yml users_database.yml");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Backend protection', BackendProtection);
|
||||||
|
describe('Verify API endpoint', VerifyEndpoint);
|
||||||
|
|
||||||
describe('Bad password', BadPassword);
|
describe('Bad password', BadPassword);
|
||||||
describe('Reset password', ResetPassword);
|
describe('Reset password', ResetPassword);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { POST_Expect401, GET_Expect401 } from "../../../helpers/utils/Requests";
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
// POST
|
||||||
|
it('should return 401 error when posting to https://login.example.com:8080/api/totp', async function() {
|
||||||
|
await POST_Expect401('https://login.example.com:8080/api/totp', { token: 'MALICIOUS_TOKEN' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 error when posting to https://login.example.com:8080/api/u2f/sign', async function() {
|
||||||
|
await POST_Expect401('https://login.example.com:8080/api/u2f/sign');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 error when posting to https://login.example.com:8080/api/u2f/register', async function() {
|
||||||
|
await POST_Expect401('https://login.example.com:8080/api/u2f/register');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// GET
|
||||||
|
it('should return 401 error on GET to https://login.example.com:8080/api/u2f/sign_request', async function() {
|
||||||
|
await GET_Expect401('https://login.example.com:8080/api/u2f/sign_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 error on GET to https://login.example.com:8080/api/u2f/register_request', async function() {
|
||||||
|
await GET_Expect401('https://login.example.com:8080/api/u2f/register_request');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Identity validation endpoints blocked to unauthenticated users', function() {
|
||||||
|
it('should return 401 error on POST to https://login.example.com:8080/api/secondfactor/u2f/identity/start', async function() {
|
||||||
|
await POST_Expect401('https://login.example.com:8080/api/secondfactor/u2f/identity/start');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 error on POST to https://login.example.com:8080/api/secondfactor/u2f/identity/finish', async function() {
|
||||||
|
await POST_Expect401('https://login.example.com:8080/api/secondfactor/u2f/identity/finish');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 error on POST to https://login.example.com:8080/api/secondfactor/totp/identity/start', async function() {
|
||||||
|
await POST_Expect401('https://login.example.com:8080/api/secondfactor/totp/identity/start');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 error on POST to https://login.example.com:8080/api/secondfactor/totp/identity/finish', async function() {
|
||||||
|
await POST_Expect401('https://login.example.com:8080/api/secondfactor/totp/identity/finish');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -4,16 +4,17 @@ import VisitPage from "../../../helpers/VisitPage";
|
||||||
import FillLoginPageWithUserAndPasswordAndClick from "../../../helpers/FillLoginPageAndClick";
|
import FillLoginPageWithUserAndPasswordAndClick from "../../../helpers/FillLoginPageAndClick";
|
||||||
import ValidateTotp from "../../../helpers/ValidateTotp";
|
import ValidateTotp from "../../../helpers/ValidateTotp";
|
||||||
import WaitRedirected from "../../../helpers/WaitRedirected";
|
import WaitRedirected from "../../../helpers/WaitRedirected";
|
||||||
|
import { WebDriver } from "selenium-webdriver";
|
||||||
|
|
||||||
export default function(this: Mocha.ISuiteCallbackContext) {
|
export default function(this: Mocha.ISuiteCallbackContext) {
|
||||||
this.timeout(15000);
|
this.timeout(20000);
|
||||||
|
|
||||||
beforeEach(async function() {
|
beforeEach(async function() {
|
||||||
this.secret = await LoginAndRegisterTotp(this.driver, "john", true);
|
this.secret = await LoginAndRegisterTotp(this.driver, "john", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should disconnect user after inactivity period", async function() {
|
it("should disconnect user after inactivity period", async function() {
|
||||||
const driver = this.driver;
|
const driver = this.driver as WebDriver;
|
||||||
await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
|
await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
|
||||||
await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', false);
|
await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', false);
|
||||||
await ValidateTotp(driver, this.secret);
|
await ValidateTotp(driver, this.secret);
|
||||||
|
@ -24,8 +25,28 @@ export default function(this: Mocha.ISuiteCallbackContext) {
|
||||||
await WaitRedirected(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
|
await WaitRedirected(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should disconnect user after cookie expiration', async function() {
|
||||||
|
const driver = this.driver as WebDriver;
|
||||||
|
await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
|
||||||
|
await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', false);
|
||||||
|
await ValidateTotp(driver, this.secret);
|
||||||
|
await WaitRedirected(driver, "https://admin.example.com:8080/secret.html");
|
||||||
|
await VisitPage(driver, "https://home.example.com:8080/");
|
||||||
|
|
||||||
|
await driver.sleep(4000);
|
||||||
|
await driver.get("https://admin.example.com:8080/secret.html");
|
||||||
|
await driver.sleep(2000);
|
||||||
|
await driver.get("https://admin.example.com:8080/secret.html");
|
||||||
|
|
||||||
|
await driver.sleep(2000);
|
||||||
|
await driver.get("https://admin.example.com:8080/secret.html");
|
||||||
|
await WaitRedirected(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With remember me checkbox checked', function() {
|
||||||
it("should keep user logged in after inactivity period", async function() {
|
it("should keep user logged in after inactivity period", async function() {
|
||||||
const driver = this.driver;
|
const driver = this.driver as WebDriver;
|
||||||
await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
|
await VisitPage(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html");
|
||||||
await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', true);
|
await FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', true);
|
||||||
await ValidateTotp(driver, this.secret);
|
await ValidateTotp(driver, this.secret);
|
||||||
|
@ -35,4 +56,5 @@ export default function(this: Mocha.ISuiteCallbackContext) {
|
||||||
await driver.get("https://admin.example.com:8080/secret.html");
|
await driver.get("https://admin.example.com:8080/secret.html");
|
||||||
await WaitRedirected(driver, "https://admin.example.com:8080/secret.html");
|
await WaitRedirected(driver, "https://admin.example.com:8080/secret.html");
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import SeleniumWebdriver from "selenium-webdriver";
|
import SeleniumWebdriver, { WebDriver } from "selenium-webdriver";
|
||||||
|
import Assert from 'assert';
|
||||||
import LoginAndRegisterTotp from '../../../helpers/LoginAndRegisterTotp';
|
import LoginAndRegisterTotp from '../../../helpers/LoginAndRegisterTotp';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,5 +27,18 @@ export default function() {
|
||||||
SeleniumWebdriver.By.className("base32-secret")),
|
SeleniumWebdriver.By.className("base32-secret")),
|
||||||
5000);
|
5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should have user and issuer in otp url", async function() {
|
||||||
|
// this.timeout(100000);
|
||||||
|
const el = await (this.driver as WebDriver).wait(
|
||||||
|
SeleniumWebdriver.until.elementLocated(
|
||||||
|
SeleniumWebdriver.By.className('otpauth-secret')), 5000);
|
||||||
|
|
||||||
|
const otpauthUrl = await el.getAttribute('innerText');
|
||||||
|
const label = 'john';
|
||||||
|
const issuer = 'example.com';
|
||||||
|
|
||||||
|
Assert(new RegExp(`^otpauth://totp/${label}\\?secret=[A-Z0-9]+&issuer=${issuer}$`).test(otpauthUrl));
|
||||||
|
})
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@ import FillField from "../../../helpers/FillField";
|
||||||
import {GetLinkFromEmail} from "../../../helpers/GetIdentityLink";
|
import {GetLinkFromEmail} from "../../../helpers/GetIdentityLink";
|
||||||
import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick";
|
import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick";
|
||||||
import IsSecondFactorStage from "../../../helpers/IsSecondFactorStage";
|
import IsSecondFactorStage from "../../../helpers/IsSecondFactorStage";
|
||||||
|
import SeeNotification from '../../../helpers/SeeNotification';
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
it("should reset password for john", async function() {
|
it("should reset password for john", async function() {
|
||||||
|
@ -16,6 +17,7 @@ export default function() {
|
||||||
await WaitRedirected(this.driver, "https://login.example.com:8080/forgot-password");
|
await WaitRedirected(this.driver, "https://login.example.com:8080/forgot-password");
|
||||||
await FillField(this.driver, "username", "john");
|
await FillField(this.driver, "username", "john");
|
||||||
await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button'));
|
await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button'));
|
||||||
|
await WaitRedirected(this.driver, 'https://login.example.com:8080/confirmation-sent');
|
||||||
|
|
||||||
await this.driver.sleep(500); // Simulate the time it takes to receive the e-mail.
|
await this.driver.sleep(500); // Simulate the time it takes to receive the e-mail.
|
||||||
const link = await GetLinkFromEmail();
|
const link = await GetLinkFromEmail();
|
||||||
|
@ -25,6 +27,36 @@ export default function() {
|
||||||
await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button'));
|
await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button'));
|
||||||
await WaitRedirected(this.driver, "https://login.example.com:8080/");
|
await WaitRedirected(this.driver, "https://login.example.com:8080/");
|
||||||
await FillLoginPageAndClick(this.driver, "john", "newpass");
|
await FillLoginPageAndClick(this.driver, "john", "newpass");
|
||||||
|
|
||||||
|
// The user reaches the second factor page using the new password.
|
||||||
await IsSecondFactorStage(this.driver);
|
await IsSecondFactorStage(this.driver);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should persuade reset password is initiated for unknown user", 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", "unknown");
|
||||||
|
await ClickOn(this.driver, SeleniumWebDriver.By.id('next-button'));
|
||||||
|
|
||||||
|
// The malicious user thinks the confirmation has been sent.
|
||||||
|
await WaitRedirected(this.driver, 'https://login.example.com:8080/confirmation-sent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should notify passwords are different in reset form", 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 WaitRedirected(this.driver, 'https://login.example.com:8080/confirmation-sent');
|
||||||
|
|
||||||
|
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", "badpass");
|
||||||
|
await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button'));
|
||||||
|
await SeeNotification(this.driver, "error", "The passwords are different.");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { GET_Expect401, GET_ExpectRedirect } from "../../../helpers/utils/Requests";
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
describe('Query without authenticated cookie', function() {
|
||||||
|
it('should get a 401 on GET to https://authelia.example.com:8080/api/verify', async function() {
|
||||||
|
await GET_Expect401('https://login.example.com:8080/api/verify');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Parameter `rd` required by Kubernetes ingress controller', async function() {
|
||||||
|
it('should redirect to https://login.example.com:8080', async function() {
|
||||||
|
await GET_ExpectRedirect('https://login.example.com:8080/api/verify?rd=https://login.example.com:8080',
|
||||||
|
'https://login.example.com:8080');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue