From efceb66ffa4948747b3ae03bc1ce44b45a0d40fd Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Sat, 9 Feb 2019 23:20:37 +0100 Subject: [PATCH] Migrate some tests to mocha. --- ...neTimePasswordRegistrationView.module.scss | 4 + .../OneTimePasswordRegistrationView.tsx | 1 + package-lock.json | 15 +++ package.json | 2 + scripts/authelia-scripts-start | 18 ++- server/src/lib/IdentityCheckMiddleware.ts | 9 +- .../routes/secondfactor/u2f/register/post.ts | 2 - .../lib/routes/secondfactor/u2f/sign/post.ts | 3 - server/src/lib/routes/verify/get.ts | 2 + server/src/lib/web_server/RestApi.ts | 8 +- shared/api.ts | 4 +- test/features/access-control.feature | 55 --------- test/features/authelia.feature | 9 -- test/features/authentication.feature | 33 ------ test/features/registration.feature | 14 --- test/features/reset-password.feature | 39 ------- test/features/restrictions.feature | 16 --- test/features/session-timeout.feature | 20 ---- test/helpers/SeeNotification.ts | 2 +- test/helpers/assertions/ObserveSecret.ts | 10 ++ test/helpers/context/AutheliaSuite.ts | 1 - test/helpers/context/WithDriver.ts | 20 +++- test/helpers/utils/Requests.ts | 52 +++++++++ test/suites/complete/index.ts | 3 + .../suites/complete/scenarii/AccessControl.ts | 107 ++++++++++++++++++ .../EnforceInternalRedirectionsOnly.ts | 12 +- .../scenarii/MongoConnectionRecovery.ts | 8 ++ test/suites/minimal/config.yml | 1 + test/suites/minimal/index.ts | 5 + .../minimal/scenarii/BackendProtection.ts | 45 ++++++++ test/suites/minimal/scenarii/Inactivity.ts | 36 ++++-- test/suites/minimal/scenarii/RegisterTotp.ts | 16 ++- test/suites/minimal/scenarii/ResetPassword.ts | 32 ++++++ .../suites/minimal/scenarii/TOTPValidation.ts | 6 +- .../suites/minimal/scenarii/VerifyEndpoint.ts | 16 +++ 35 files changed, 398 insertions(+), 228 deletions(-) delete mode 100644 test/features/access-control.feature delete mode 100644 test/features/authelia.feature delete mode 100644 test/features/registration.feature delete mode 100644 test/features/reset-password.feature delete mode 100644 test/features/restrictions.feature delete mode 100644 test/features/session-timeout.feature create mode 100644 test/helpers/assertions/ObserveSecret.ts create mode 100644 test/helpers/utils/Requests.ts create mode 100644 test/suites/complete/scenarii/AccessControl.ts create mode 100644 test/suites/minimal/scenarii/BackendProtection.ts create mode 100644 test/suites/minimal/scenarii/VerifyEndpoint.ts diff --git a/client/src/assets/scss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.module.scss b/client/src/assets/scss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.module.scss index f1fdac36..35c86026 100644 --- a/client/src/assets/scss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.module.scss +++ b/client/src/assets/scss/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.module.scss @@ -20,6 +20,10 @@ word-wrap: break-word; } +.otpauthContainer { + display: none; +} + .text { text-align: center; } diff --git a/client/src/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.tsx b/client/src/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.tsx index a3411f52..15e55cd0 100644 --- a/client/src/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.tsx +++ b/client/src/views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView.tsx @@ -58,6 +58,7 @@ class OneTimePasswordRegistrationView extends Component {
+
{secret.otpauth_url}
{secret.base32_secret}
diff --git a/package-lock.json b/package-lock.json index 82ee472e..bfbba857 100644 --- a/package-lock.json +++ b/package-lock.json @@ -358,6 +358,15 @@ "integrity": "sha512-J7nx6JzxmtT4zyvfLipYL7jNaxvlCWpyG7JhhCQ4fQyG+AGfovAHoYR55TFx+X8akfkUJYpt5JG6GPeFMjZaCQ==", "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": { "version": "4.6.0", "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", "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": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.4.tgz", diff --git a/package.json b/package.json index 0bba9e9d..770799f4 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@types/mockdate": "^2.0.0", "@types/mongodb": "^3.0.9", "@types/nedb": "^1.8.3", + "@types/node-fetch": "^2.1.4", "@types/nodemailer": "^4.6.0", "@types/nodemailer-direct-transport": "^1.0.31", "@types/nodemailer-smtp-transport": "^2.7.4", @@ -98,6 +99,7 @@ "jquery": "^3.2.1", "mocha": "^5.2.0", "mockdate": "^2.0.1", + "node-fetch": "^2.3.0", "nodemon": "^1.18.9", "nyc": "^13.1.0", "query-string": "^6.0.0", diff --git a/scripts/authelia-scripts-start b/scripts/authelia-scripts-start index 1b4373a4..39f32749 100755 --- a/scripts/authelia-scripts-start +++ b/scripts/authelia-scripts-start @@ -24,8 +24,8 @@ var tsWatcher = chokidar.watch(['server', 'shared/**/*.ts', 'node_modules'], { // Properly cleanup server and client if ctrl-c is hit process.on('SIGINT', function() { - killServer(); - killClient(); + killServer(() => {}); + killClient(() => {}); fs.unlinkSync(ENVIRONMENT_FILENAME); process.exit(); }); @@ -60,21 +60,31 @@ function startClient() { function killServer(onExit) { if (serverProcess) { - process.kill(-serverProcess.pid); serverProcess.on('exit', () => { serverProcess = undefined; onExit(); }); + try { + process.kill(-serverProcess.pid); + } catch (e) { + console.error(e); + onExit(); + } } } function killClient(onExit) { if (clientProcess) { - process.kill(-clientProcess.pid); clientProcess.on('exit', () => { clientProcess = undefined; onExit(); }); + try { + process.kill(-clientProcess.pid); + } catch (e) { + console.error(e); + onExit(); + } } } diff --git a/server/src/lib/IdentityCheckMiddleware.ts b/server/src/lib/IdentityCheckMiddleware.ts index 15605493..cedd16e2 100644 --- a/server/src/lib/IdentityCheckMiddleware.ts +++ b/server/src/lib/IdentityCheckMiddleware.ts @@ -42,9 +42,9 @@ export function register(app: Express.Application, vars: ServerVariables) { app.post(pre_validation_endpoint, - get_start_validation(handler, post_validation_endpoint, vars)); + post_start_validation(handler, vars)); app.post(post_validation_endpoint, - get_finish_validation(handler, vars)); + post_finish_validation(handler, vars)); } function checkIdentityToken(req: Express.Request, identityToken: string) @@ -55,7 +55,7 @@ function checkIdentityToken(req: Express.Request, identityToken: string) return BluebirdPromise.resolve(); } -export function get_finish_validation(handler: IdentityValidable, +export function post_finish_validation(handler: IdentityValidable, vars: ServerVariables) : Express.RequestHandler { @@ -88,8 +88,7 @@ export function get_finish_validation(handler: IdentityValidable, }; } -export function get_start_validation(handler: IdentityValidable, - postValidationEndpoint: string, +export function post_start_validation(handler: IdentityValidable, vars: ServerVariables) : Express.RequestHandler { return function (req: Express.Request, res: Express.Response) diff --git a/server/src/lib/routes/secondfactor/u2f/register/post.ts b/server/src/lib/routes/secondfactor/u2f/register/post.ts index 7296ccbe..c7ac656d 100644 --- a/server/src/lib/routes/secondfactor/u2f/register/post.ts +++ b/server/src/lib/routes/secondfactor/u2f/register/post.ts @@ -1,5 +1,3 @@ - -import { UserDataStore } from "../../../../storage/UserDataStore"; import objectPath = require("object-path"); import u2f_common = require("../U2FCommon"); import BluebirdPromise = require("bluebird"); diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.ts index 2f333c85..87c3ce1e 100644 --- a/server/src/lib/routes/secondfactor/u2f/sign/post.ts +++ b/server/src/lib/routes/secondfactor/u2f/sign/post.ts @@ -3,11 +3,8 @@ import objectPath = require("object-path"); import u2f_common = require("../U2FCommon"); import BluebirdPromise = require("bluebird"); import express = require("express"); -import { UserDataStore } from "../../../../storage/UserDataStore"; import { U2FRegistrationDocument } from "../../../../storage/U2FRegistrationDocument"; -import { Winston } from "../../../../../../types/Dependencies"; import U2f = require("u2f"); -import exceptions = require("../../../../Exceptions"); import redirect from "../../redirect"; import ErrorReplies = require("../../../../ErrorReplies"); import { ServerVariables } from "../../../../ServerVariables"; diff --git a/server/src/lib/routes/verify/get.ts b/server/src/lib/routes/verify/get.ts index f7386169..e5fe87fc 100644 --- a/server/src/lib/routes/verify/get.ts +++ b/server/src/lib/routes/verify/get.ts @@ -78,6 +78,8 @@ export default function (vars: ServerVariables) { ErrorReplies.replyWithError401(req, res, vars.logger)) // The user is not yet authenticated -> 401 .catch((err) => { + // This redirect parameter is used in Kubernetes to annotate the ingress with + // the url to the authentication portal. const redirectUrl = getRedirectParam(req); if (redirectUrl) { ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err); diff --git a/server/src/lib/web_server/RestApi.ts b/server/src/lib/web_server/RestApi.ts index 75bd78fd..de17f365 100644 --- a/server/src/lib/web_server/RestApi.ts +++ b/server/src/lib/web_server/RestApi.ts @@ -30,15 +30,15 @@ function setupTotp(app: Express.Application, vars: ServerVariables) { RequireValidatedFirstFactor.middleware(vars.logger), 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)); - app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + app.post(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_POST, RequireValidatedFirstFactor.middleware(vars.logger)); IdentityCheckMiddleware.register(app, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET, - Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_POST, + Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_POST, new TOTPRegistrationIdentityHandler(vars.logger, vars.userDataStore, vars.totpHandler, vars.config.totp), vars); diff --git a/shared/api.ts b/shared/api.ts index 522a420d..dacb04f6 100644 --- a/shared/api.ts +++ b/shared/api.ts @@ -143,7 +143,7 @@ export const SECOND_FACTOR_U2F_IDENTITY_FINISH_POST = "/api/secondfactor/u2f/ide * * @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. * 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"; diff --git a/test/features/access-control.feature b/test/features/access-control.feature deleted file mode 100644 index 0e513ea1..00000000 --- a/test/features/access-control.feature +++ /dev/null @@ -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" diff --git a/test/features/authelia.feature b/test/features/authelia.feature deleted file mode 100644 index dc993c2b..00000000 --- a/test/features/authelia.feature +++ /dev/null @@ -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" \ No newline at end of file diff --git a/test/features/authentication.feature b/test/features/authentication.feature index 851c7e5e..0688916f 100644 --- a/test/features/authentication.feature +++ b/test/features/authentication.feature @@ -1,38 +1,5 @@ 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 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/" diff --git a/test/features/registration.feature b/test/features/registration.feature deleted file mode 100644 index c1c92b52..00000000 --- a/test/features/registration.feature +++ /dev/null @@ -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" \ No newline at end of file diff --git a/test/features/reset-password.feature b/test/features/reset-password.feature deleted file mode 100644 index d449fad7..00000000 --- a/test/features/reset-password.feature +++ /dev/null @@ -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." diff --git a/test/features/restrictions.feature b/test/features/restrictions.feature deleted file mode 100644 index 97c85a34..00000000 --- a/test/features/restrictions.feature +++ /dev/null @@ -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 | diff --git a/test/features/session-timeout.feature b/test/features/session-timeout.feature deleted file mode 100644 index 09bfb1fd..00000000 --- a/test/features/session-timeout.feature +++ /dev/null @@ -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" diff --git a/test/helpers/SeeNotification.ts b/test/helpers/SeeNotification.ts index a395b4a7..2288ecc8 100644 --- a/test/helpers/SeeNotification.ts +++ b/test/helpers/SeeNotification.ts @@ -1,4 +1,4 @@ -import SeleniumWebdriver, { ThenableWebDriver, WebDriver } from "selenium-webdriver"; +import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; import Assert = require("assert"); export default async function(driver: WebDriver, type: string, message: string) { diff --git a/test/helpers/assertions/ObserveSecret.ts b/test/helpers/assertions/ObserveSecret.ts new file mode 100644 index 00000000..583c37c8 --- /dev/null +++ b/test/helpers/assertions/ObserveSecret.ts @@ -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); +} \ No newline at end of file diff --git a/test/helpers/context/AutheliaSuite.ts b/test/helpers/context/AutheliaSuite.ts index 32f2a6ed..f407343c 100644 --- a/test/helpers/context/AutheliaSuite.ts +++ b/test/helpers/context/AutheliaSuite.ts @@ -18,7 +18,6 @@ function AutheliaSuiteBase(description: string, configPath: string, } return context('Suite: ' + description, function(this: Mocha.ISuiteCallbackContext) { - WithDriver.call(this); cb.call(this); }); } diff --git a/test/helpers/context/WithDriver.ts b/test/helpers/context/WithDriver.ts index c9384bcd..c7c6960a 100644 --- a/test/helpers/context/WithDriver.ts +++ b/test/helpers/context/WithDriver.ts @@ -2,22 +2,30 @@ require("chromedriver"); import chrome from 'selenium-webdriver/chrome'; import SeleniumWebdriver from "selenium-webdriver"; -export default function() { +export default function(forEach: boolean = false) { let options = new chrome.Options(); if (process.env['HEADLESS'] == 'y') { options = options.headless(); } - beforeEach(function() { + function beforeBlock(this: Mocha.IHookCallbackContext) { const driver = new SeleniumWebdriver.Builder() .forBrowser("chrome") .setChromeOptions(options) .build(); this.driver = driver; - }); + } - afterEach(function() { - this.driver.quit(); - }); + function afterBlock(this: Mocha.IHookCallbackContext) { + return this.driver.quit(); + } + + if (forEach) { + beforeEach(beforeBlock); + afterEach(afterBlock); + } else { + before(beforeBlock); + after(afterBlock); + } } \ No newline at end of file diff --git a/test/helpers/utils/Requests.ts b/test/helpers/utils/Requests.ts new file mode 100644 index 00000000..41d76b01 --- /dev/null +++ b/test/helpers/utils/Requests.ts @@ -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'); +} \ No newline at end of file diff --git a/test/suites/complete/index.ts b/test/suites/complete/index.ts index 7705c7c8..e7932b62 100644 --- a/test/suites/complete/index.ts +++ b/test/suites/complete/index.ts @@ -1,10 +1,13 @@ import AutheliaSuite from "../../helpers/context/AutheliaSuite"; import MongoConnectionRecovery from "./scenarii/MongoConnectionRecovery"; import EnforceInternalRedirectionsOnly from "./scenarii/EnforceInternalRedirectionsOnly"; +import AccessControl from "./scenarii/AccessControl"; AutheliaSuite('Complete configuration', __dirname + '/config.yml', function() { this.timeout(10000); + describe('Access control', AccessControl); + describe('Mongo broken connection recovery', MongoConnectionRecovery); describe('Enforce internal redirections only', EnforceInternalRedirectionsOnly); }); \ No newline at end of file diff --git a/test/suites/complete/scenarii/AccessControl.ts b/test/suites/complete/scenarii/AccessControl.ts new file mode 100644 index 00000000..e3cf844b --- /dev/null +++ b/test/suites/complete/scenarii/AccessControl.ts @@ -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'); + }) +} \ No newline at end of file diff --git a/test/suites/complete/scenarii/EnforceInternalRedirectionsOnly.ts b/test/suites/complete/scenarii/EnforceInternalRedirectionsOnly.ts index b1d7cbbc..a7c34b56 100644 --- a/test/suites/complete/scenarii/EnforceInternalRedirectionsOnly.ts +++ b/test/suites/complete/scenarii/EnforceInternalRedirectionsOnly.ts @@ -5,6 +5,7 @@ import ValidateTotp from "../../../helpers/ValidateTotp"; import Logout from "../../../helpers/Logout"; import WaitRedirected from "../../../helpers/WaitRedirected"; import IsAlreadyAuthenticatedStage from "../../../helpers/IsAlreadyAuthenticatedStage"; +import WithDriver from "../../../helpers/context/WithDriver"; /* * 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. */ export default function() { + WithDriver(true); describe("Only redirection to a subdomain of the protected domain should be allowed", function() { this.timeout(10000); let secret: string; @@ -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 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"); + }); + describe('Cannot redirect to http://public.example.com:8080', function() { // Do not redirect to http website 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. CanRedirectTo("https://public.example.com:8080/"); }); diff --git a/test/suites/complete/scenarii/MongoConnectionRecovery.ts b/test/suites/complete/scenarii/MongoConnectionRecovery.ts index 9420031f..1f1239e1 100644 --- a/test/suites/complete/scenarii/MongoConnectionRecovery.ts +++ b/test/suites/complete/scenarii/MongoConnectionRecovery.ts @@ -1,8 +1,16 @@ import LoginAndRegisterTotp from "../../../helpers/LoginAndRegisterTotp"; import FullLogin from "../../../helpers/FullLogin"; import child_process from 'child_process'; +import WithDriver from "../../../helpers/context/WithDriver"; +import Logout from "../../../helpers/Logout"; export default function() { + after(async function() { + await Logout(this.driver); + }) + + WithDriver(); + it("should be able to login after mongo restarts", async function() { this.timeout(30000); diff --git a/test/suites/minimal/config.yml b/test/suites/minimal/config.yml index 11512290..a1d13552 100644 --- a/test/suites/minimal/config.yml +++ b/test/suites/minimal/config.yml @@ -14,6 +14,7 @@ session: secret: unsecure_session_secret domain: example.com inactivity: 5000 + expiration: 8000 # Configuration of the storage backend used to store data and secrets. i.e. totp data storage: diff --git a/test/suites/minimal/index.ts b/test/suites/minimal/index.ts index d986f3e7..d68ed4e9 100644 --- a/test/suites/minimal/index.ts +++ b/test/suites/minimal/index.ts @@ -7,6 +7,8 @@ import RegisterTotp from './scenarii/RegisterTotp'; import ResetPassword from './scenarii/ResetPassword'; import TOTPValidation from './scenarii/TOTPValidation'; import Inactivity from './scenarii/Inactivity'; +import BackendProtection from './scenarii/BackendProtection'; +import VerifyEndpoint from './scenarii/VerifyEndpoint'; 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"); }); + describe('Backend protection', BackendProtection); + describe('Verify API endpoint', VerifyEndpoint); + describe('Bad password', BadPassword); describe('Reset password', ResetPassword); diff --git a/test/suites/minimal/scenarii/BackendProtection.ts b/test/suites/minimal/scenarii/BackendProtection.ts new file mode 100644 index 00000000..8de7c4ff --- /dev/null +++ b/test/suites/minimal/scenarii/BackendProtection.ts @@ -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'); + }); + }); +} \ No newline at end of file diff --git a/test/suites/minimal/scenarii/Inactivity.ts b/test/suites/minimal/scenarii/Inactivity.ts index 959bbfde..78f202b9 100644 --- a/test/suites/minimal/scenarii/Inactivity.ts +++ b/test/suites/minimal/scenarii/Inactivity.ts @@ -4,16 +4,17 @@ import VisitPage from "../../../helpers/VisitPage"; import FillLoginPageWithUserAndPasswordAndClick from "../../../helpers/FillLoginPageAndClick"; import ValidateTotp from "../../../helpers/ValidateTotp"; import WaitRedirected from "../../../helpers/WaitRedirected"; +import { WebDriver } from "selenium-webdriver"; export default function(this: Mocha.ISuiteCallbackContext) { - this.timeout(15000); + this.timeout(20000); beforeEach(async function() { this.secret = await LoginAndRegisterTotp(this.driver, "john", true); }); 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 FillLoginPageWithUserAndPasswordAndClick(driver, 'john', 'password', false); await ValidateTotp(driver, this.secret); @@ -24,15 +25,36 @@ export default function(this: Mocha.ISuiteCallbackContext) { await WaitRedirected(driver, "https://login.example.com:8080/?rd=https://admin.example.com:8080/secret.html"); }); - it("should keep user logged in after inactivity period", async function() { - const driver = this.driver; + 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', true); + 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(6000); + + await driver.sleep(4000); await driver.get("https://admin.example.com:8080/secret.html"); - await WaitRedirected(driver, "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() { + 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', true); + 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(6000); + await driver.get("https://admin.example.com:8080/secret.html"); + await WaitRedirected(driver, "https://admin.example.com:8080/secret.html"); + }); }); } \ No newline at end of file diff --git a/test/suites/minimal/scenarii/RegisterTotp.ts b/test/suites/minimal/scenarii/RegisterTotp.ts index 1d1350b5..96dbc5f1 100644 --- a/test/suites/minimal/scenarii/RegisterTotp.ts +++ b/test/suites/minimal/scenarii/RegisterTotp.ts @@ -1,4 +1,5 @@ -import SeleniumWebdriver from "selenium-webdriver"; +import SeleniumWebdriver, { WebDriver } from "selenium-webdriver"; +import Assert from 'assert'; import LoginAndRegisterTotp from '../../../helpers/LoginAndRegisterTotp'; /** @@ -26,5 +27,18 @@ export default function() { SeleniumWebdriver.By.className("base32-secret")), 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)); + }) }); }; diff --git a/test/suites/minimal/scenarii/ResetPassword.ts b/test/suites/minimal/scenarii/ResetPassword.ts index eb479e36..3217c9e6 100644 --- a/test/suites/minimal/scenarii/ResetPassword.ts +++ b/test/suites/minimal/scenarii/ResetPassword.ts @@ -8,6 +8,7 @@ import FillField from "../../../helpers/FillField"; import {GetLinkFromEmail} from "../../../helpers/GetIdentityLink"; import FillLoginPageAndClick from "../../../helpers/FillLoginPageAndClick"; import IsSecondFactorStage from "../../../helpers/IsSecondFactorStage"; +import SeeNotification from '../../../helpers/SeeNotification'; export default 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 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(); @@ -25,6 +27,36 @@ export default function() { await ClickOn(this.driver, SeleniumWebDriver.By.id('reset-button')); await WaitRedirected(this.driver, "https://login.example.com:8080/"); await FillLoginPageAndClick(this.driver, "john", "newpass"); + + // The user reaches the second factor page using the new password. 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."); + }); } diff --git a/test/suites/minimal/scenarii/TOTPValidation.ts b/test/suites/minimal/scenarii/TOTPValidation.ts index d011d3da..99a60f5c 100644 --- a/test/suites/minimal/scenarii/TOTPValidation.ts +++ b/test/suites/minimal/scenarii/TOTPValidation.ts @@ -39,9 +39,9 @@ export default 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); + 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() { diff --git a/test/suites/minimal/scenarii/VerifyEndpoint.ts b/test/suites/minimal/scenarii/VerifyEndpoint.ts new file mode 100644 index 00000000..1629d679 --- /dev/null +++ b/test/suites/minimal/scenarii/VerifyEndpoint.ts @@ -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'); + }); + }); + }); +} \ No newline at end of file