From 56fdc40290f77ccfe571328cd84a46eb480def3e Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Tue, 10 Oct 2017 23:03:30 +0200 Subject: [PATCH] Every public endpoints return 200 with harmonized error messages or 401 Now, /verify can return 401 or 403 depending on the user authentication. Every public API endpoints and pages return 200 with error message in JSON body or 401 if the user is not authorized. This policy makes it complicated for an attacker to know what is the source of the failure and hide server-side bugs (not returning 500), bugs being potential threats. --- Gruntfile.js | 2 +- .../lib/firstfactor/FirstFactorValidator.ts | 11 ++- client/src/lib/firstfactor/index.ts | 9 +- .../lib/reset-password/reset-password-form.ts | 17 ++-- .../reset-password/reset-password-request.ts | 18 ++-- client/src/lib/secondfactor/TOTPValidator.ts | 36 ++++---- client/src/lib/secondfactor/U2FValidator.ts | 91 ++++++++++--------- client/src/lib/secondfactor/index.ts | 11 +-- client/src/lib/u2f-register/u2f-register.ts | 13 ++- .../firstfactor/FirstFactorValidator.test.ts | 2 +- .../test/secondfactor/TOTPValidator.test.ts | 46 +++++----- package.json | 6 +- server/src/lib/ErrorReplies.ts | 10 +- server/src/lib/FirstFactorValidator.ts | 4 +- server/src/lib/IdentityCheckMiddleware.ts | 27 +++--- server/src/lib/routes/FirstFactorBlocker.ts | 3 +- server/src/lib/routes/firstfactor/post.ts | 12 +-- .../lib/routes/password-reset/form/post.ts | 5 +- .../src/lib/routes/secondfactor/redirect.ts | 3 +- .../totp/identity/RegistrationHandler.ts | 5 +- .../lib/routes/secondfactor/totp/sign/post.ts | 9 +- .../routes/secondfactor/u2f/register/post.ts | 8 +- .../secondfactor/u2f/register_request/get.ts | 4 +- .../lib/routes/secondfactor/u2f/sign/post.ts | 4 +- .../secondfactor/u2f/sign_request/get.ts | 5 +- server/src/lib/routes/verify/get.ts | 5 +- server/src/views/layout/layout.pug | 2 +- server/test/IdentityCheckMiddleware.test.ts | 30 +++--- server/test/routes/firstfactor/post.test.ts | 25 +++-- .../test/routes/password-reset/post.test.ts | 18 ++-- .../secondfactor/u2f/register/post.test.ts | 28 ++++-- .../u2f/register_request/get.test.ts | 21 +++-- .../routes/secondfactor/u2f/sign/post.test.ts | 4 +- server/test/server/PrivatePages.ts | 25 ++--- shared/UserMessages.ts | 23 +++++ test/features/authentication.feature | 4 +- test/features/regulation.feature | 14 +-- test/features/reset-password.feature | 6 ++ test/features/restrictions.feature | 8 +- 39 files changed, 325 insertions(+), 249 deletions(-) create mode 100644 shared/UserMessages.ts diff --git a/Gruntfile.js b/Gruntfile.js index cf7c78a1b..713eb3c4b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -53,7 +53,7 @@ module.exports = function (grunt) { }, "include-minified-script": { cmd: "sed", - args: ["-i", "s/authelia\.js/authelia.min.js/", `${buildDir}/server/src/views/layout/layout.pug`] + args: ["-i", "s/authelia.\(js\|css\)/authelia.min.\1/", `${buildDir}/server/src/views/layout/layout.pug`] } }, copy: { diff --git a/client/src/lib/firstfactor/FirstFactorValidator.ts b/client/src/lib/firstfactor/FirstFactorValidator.ts index 175f3c2c8..60a4d17c3 100644 --- a/client/src/lib/firstfactor/FirstFactorValidator.ts +++ b/client/src/lib/firstfactor/FirstFactorValidator.ts @@ -3,6 +3,7 @@ import BluebirdPromise = require("bluebird"); import Endpoints = require("../../../../shared/api"); import Constants = require("../../../../shared/constants"); import Util = require("util"); +import UserMessages = require("../../../../shared/UserMessages"); export function validate(username: string, password: string, redirectUrl: string, $: JQueryStatic): BluebirdPromise { @@ -24,11 +25,15 @@ export function validate(username: string, password: string, password: password, } }) - .done(function (data: { redirect: string }) { - resolve(data.redirect); + .done(function (body: any) { + if (body && body.error) { + reject(new Error(body.error)); + return; + } + resolve(body.redirect); }) .fail(function (xhr: JQueryXHR, textStatus: string) { - reject(new Error("Authetication failed. Please check your credentials.")); + reject(new Error(UserMessages.AUTHENTICATION_FAILED)); }); }); } diff --git a/client/src/lib/firstfactor/index.ts b/client/src/lib/firstfactor/index.ts index 91de7e001..0c5f9828d 100644 --- a/client/src/lib/firstfactor/index.ts +++ b/client/src/lib/firstfactor/index.ts @@ -5,6 +5,7 @@ import { Notifier } from "../Notifier"; import { QueryParametersRetriever } from "../QueryParametersRetriever"; import Constants = require("../../../../shared/constants"); import Endpoints = require("../../../../shared/api"); +import UserMessages = require("../../../../shared/UserMessages"); export default function (window: Window, $: JQueryStatic, firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) { @@ -23,18 +24,14 @@ export default function (window: Window, $: JQueryStatic, } function onFirstFactorSuccess(redirectUrl: string) { - jslogger.debug("First factor validated."); - window.location.href = redirectUrl; + window.location.href = redirectUrl; } function onFirstFactorFailure(err: Error) { - jslogger.debug("First factor failed."); - notifier.error("Authentication failed. Please double check your credentials."); + notifier.error(UserMessages.AUTHENTICATION_FAILED); } - $(window.document).ready(function () { - jslogger.info("Enter first factor"); $("form").on("submit", onFormSubmitted); }); } diff --git a/client/src/lib/reset-password/reset-password-form.ts b/client/src/lib/reset-password/reset-password-form.ts index 12f60d777..888026f62 100644 --- a/client/src/lib/reset-password/reset-password-form.ts +++ b/client/src/lib/reset-password/reset-password-form.ts @@ -1,6 +1,8 @@ import BluebirdPromise = require("bluebird"); import Endpoints = require("../../../../shared/api"); +import UserMessages = require("../../../../shared/UserMessages"); + import Constants = require("./constants"); import { Notifier } from "../Notifier"; @@ -12,8 +14,12 @@ export default function (window: Window, $: JQueryStatic) { $.post(Endpoints.RESET_PASSWORD_FORM_POST, { password: newPassword, }) - .done(function (data) { - resolve(data); + .done(function (body: any) { + if (body && body.error) { + reject(new Error(body.error)); + return; + } + resolve(body); }) .fail(function (xhr, status) { reject(status); @@ -26,22 +32,21 @@ export default function (window: Window, $: JQueryStatic) { const password2 = $("#password2").val(); if (!password1 || !password2) { - notifier.warning("You must enter your new password twice."); + notifier.warning(UserMessages.MISSING_PASSWORD); return false; } if (password1 != password2) { - notifier.warning("The passwords are different."); + notifier.warning(UserMessages.DIFFERENT_PASSWORDS); return false; } modifyPassword(password1) .then(function () { - notifier.success("Your password has been changed. Please log in again."); window.location.href = Endpoints.FIRST_FACTOR_GET; }) .error(function () { - notifier.warning("An error occurred during password reset. Your password has not been changed."); + notifier.error(UserMessages.RESET_PASSWORD_FAILED); }); return false; } diff --git a/client/src/lib/reset-password/reset-password-request.ts b/client/src/lib/reset-password/reset-password-request.ts index 0685e1760..426bad34b 100644 --- a/client/src/lib/reset-password/reset-password-request.ts +++ b/client/src/lib/reset-password/reset-password-request.ts @@ -2,11 +2,12 @@ import BluebirdPromise = require("bluebird"); import Endpoints = require("../../../../shared/api"); +import UserMessages = require("../../../../shared/UserMessages"); import Constants = require("./constants"); import jslogger = require("js-logger"); import { Notifier } from "../Notifier"; -export default function(window: Window, $: JQueryStatic) { +export default function (window: Window, $: JQueryStatic) { const notifier = new Notifier(".notification", $); function requestPasswordReset(username: string) { @@ -14,7 +15,11 @@ export default function(window: Window, $: JQueryStatic) { $.get(Endpoints.RESET_PASSWORD_IDENTITY_START_GET, { userid: username, }) - .done(function () { + .done(function (body: any) { + if (body && body.error) { + reject(new Error(body.error)); + return; + } resolve(); }) .fail(function (xhr: JQueryXHR, textStatus: string) { @@ -27,25 +32,24 @@ export default function(window: Window, $: JQueryStatic) { const username = $("#username").val(); if (!username) { - notifier.warning("You must provide your username to reset your password."); + notifier.warning(UserMessages.MISSING_USERNAME); return; } requestPasswordReset(username) .then(function () { - notifier.success("An email has been sent to you. Follow the link to change your password."); + notifier.success(UserMessages.MAIL_SENT); setTimeout(function () { window.location.replace(Endpoints.FIRST_FACTOR_GET); }, 1000); }) .error(function () { - notifier.warning("Are you sure this is your username?"); + notifier.error(UserMessages.MAIL_NOT_SENT); }); - return false; + return false; } $(document).ready(function () { - jslogger.debug("Reset password request form setup"); $(Constants.FORM_SELECTOR).on("submit", onFormSubmitted); }); } diff --git a/client/src/lib/secondfactor/TOTPValidator.ts b/client/src/lib/secondfactor/TOTPValidator.ts index 824b591a7..5d845edc5 100644 --- a/client/src/lib/secondfactor/TOTPValidator.ts +++ b/client/src/lib/secondfactor/TOTPValidator.ts @@ -3,20 +3,24 @@ import BluebirdPromise = require("bluebird"); import Endpoints = require("../../../../shared/api"); export function validate(token: string, $: JQueryStatic): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - $.ajax({ - url: Endpoints.SECOND_FACTOR_TOTP_POST, - data: { - token: token, - }, - method: "POST", - dataType: "json" - } as JQueryAjaxSettings) - .done(function (data: any) { - resolve(data); - }) - .fail(function (xhr: JQueryXHR, textStatus: string) { - reject(new Error(textStatus)); - }); - }); + return new BluebirdPromise(function (resolve, reject) { + $.ajax({ + url: Endpoints.SECOND_FACTOR_TOTP_POST, + data: { + token: token, + }, + method: "POST", + dataType: "json" + } as JQueryAjaxSettings) + .done(function (body: any) { + if (body && body.error) { + reject(new Error(body.error)); + return; + } + resolve(body); + }) + .fail(function (xhr: JQueryXHR, textStatus: string) { + reject(new Error(textStatus)); + }); + }); } \ No newline at end of file diff --git a/client/src/lib/secondfactor/U2FValidator.ts b/client/src/lib/secondfactor/U2FValidator.ts index 66c7751aa..0806e2418 100644 --- a/client/src/lib/secondfactor/U2FValidator.ts +++ b/client/src/lib/secondfactor/U2FValidator.ts @@ -4,59 +4,64 @@ import U2f = require("u2f"); import BluebirdPromise = require("bluebird"); import { SignMessage } from "../../../../shared/SignMessage"; import Endpoints = require("../../../../shared/api"); +import UserMessages = require("../../../../shared/UserMessages"); import { INotifier } from "../INotifier"; function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQueryStatic): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - $.ajax({ - url: Endpoints.SECOND_FACTOR_U2F_SIGN_POST, - data: responseData, - method: "POST", - dataType: "json" - } as JQueryAjaxSettings) - .done(function (data) { - resolve(data); - }) - .fail(function (xhr: JQueryXHR, textStatus: string) { - reject(new Error(textStatus)); - }); - }); + return new BluebirdPromise(function (resolve, reject) { + $.ajax({ + url: Endpoints.SECOND_FACTOR_U2F_SIGN_POST, + data: responseData, + method: "POST", + dataType: "json" + } as JQueryAjaxSettings) + .done(function (body: any) { + if (body && body.error) { + reject(new Error(body.error)); + return; + } + resolve(body); + }) + .fail(function (xhr: JQueryXHR, textStatus: string) { + reject(new Error(textStatus)); + }); + }); } function startU2fAuthentication($: JQueryStatic, notifier: INotifier, u2fApi: typeof U2fApi): BluebirdPromise { - return new BluebirdPromise(function (resolve, reject) { - $.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, undefined, "json") - .done(function (signResponse: SignMessage) { - notifier.info("Please touch the token"); + return new BluebirdPromise(function (resolve, reject) { + $.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, undefined, "json") + .done(function (signResponse: SignMessage) { + notifier.info(UserMessages.PLEASE_TOUCH_TOKEN); - const signRequest: U2fApi.SignRequest = { - appId: signResponse.request.appId, - challenge: signResponse.request.challenge, - keyHandle: signResponse.keyHandle, // linked to the client session cookie - version: "U2F_V2" - }; + const signRequest: U2fApi.SignRequest = { + appId: signResponse.request.appId, + challenge: signResponse.request.challenge, + keyHandle: signResponse.keyHandle, // linked to the client session cookie + version: "U2F_V2" + }; - u2fApi.sign([signRequest], 60) - .then(function (signResponse: U2fApi.SignResponse) { - finishU2fAuthentication(signResponse, $) - .then(function (data) { - resolve(data); - }, function (err) { - notifier.error("Error when finish U2F transaction"); - reject(err); - }); - }) - .catch(function (err: Error) { - reject(err); - }); - }) - .fail(function (xhr: JQueryXHR, textStatus: string) { - reject(new Error(textStatus)); - }); - }); + u2fApi.sign([signRequest], 60) + .then(function (signResponse: U2fApi.SignResponse) { + finishU2fAuthentication(signResponse, $) + .then(function (data) { + resolve(data); + }, function (err) { + notifier.error(UserMessages.U2F_TRANSACTION_FINISH_FAILED); + reject(err); + }); + }) + .catch(function (err: Error) { + reject(err); + }); + }) + .fail(function (xhr: JQueryXHR, textStatus: string) { + reject(new Error(textStatus)); + }); + }); } export function validate($: JQueryStatic, notifier: INotifier, u2fApi: typeof U2fApi): BluebirdPromise { - return startU2fAuthentication($, notifier, u2fApi); + return startU2fAuthentication($, notifier, u2fApi); } diff --git a/client/src/lib/secondfactor/index.ts b/client/src/lib/secondfactor/index.ts index 52875254e..c829dea8b 100644 --- a/client/src/lib/secondfactor/index.ts +++ b/client/src/lib/secondfactor/index.ts @@ -9,7 +9,7 @@ import { Notifier } from "../Notifier"; import { QueryParametersRetriever } from "../QueryParametersRetriever"; import Endpoints = require("../../../../shared/api"); import ServerConstants = require("../../../../shared/constants"); - +import UserMessages = require("../../../../shared/UserMessages"); export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) { const notifierTotp = new Notifier(".notification-totp", $); @@ -20,7 +20,7 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) if (redirectUrl) window.location.href = redirectUrl; else - notifier.success("Authentication succeeded. You can now access your services."); + notifier.success(UserMessages.AUTHENTICATION_SUCCEEDED); } function onSecondFactorTotpSuccess(data: any) { @@ -28,7 +28,7 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) } function onSecondFactorTotpFailure(err: Error) { - notifierTotp.error("Problem with TOTP validation."); + notifierTotp.error(UserMessages.AUTHENTICATION_TOTP_FAILED); } function onU2fAuthenticationSuccess(data: any) { @@ -36,13 +36,11 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) } function onU2fAuthenticationFailure() { - notifierU2f.error("Problem with U2F validation. Did you register before authenticating?"); + notifierU2f.error(UserMessages.AUTHENTICATION_U2F_FAILED); } function onTOTPFormSubmitted(): boolean { const token = $(Constants.TOTP_TOKEN_SELECTOR).val(); - jslogger.debug("TOTP token is %s", token); - TOTPValidator.validate(token, $) .then(onSecondFactorTotpSuccess) .catch(onSecondFactorTotpFailure); @@ -50,7 +48,6 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) } function onU2FFormSubmitted(): boolean { - jslogger.debug("Start U2F authentication"); U2FValidator.validate($, notifierU2f, U2fApi) .then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure); return false; diff --git a/client/src/lib/u2f-register/u2f-register.ts b/client/src/lib/u2f-register/u2f-register.ts index f895bbf60..f66e3c80e 100644 --- a/client/src/lib/u2f-register/u2f-register.ts +++ b/client/src/lib/u2f-register/u2f-register.ts @@ -5,6 +5,7 @@ import u2fApi = require("u2f-api"); import jslogger = require("js-logger"); import { Notifier } from "../Notifier"; import Endpoints = require("../../../../shared/api"); +import UserMessages = require("../../../../shared/UserMessages"); export default function (window: Window, $: JQueryStatic) { const notifier = new Notifier(".notification", $); @@ -16,8 +17,12 @@ export default function (window: Window, $: JQueryStatic) { return new BluebirdPromise(function (resolve, reject) { $.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, registrationData, undefined, "json") - .done(function (data) { - resolve(data.redirection_url); + .done(function (body: any) { + if (body && body.error) { + reject(new Error(body.error)); + return; + } + resolve(body.redirection_url); }) .fail(function (xhr, status) { reject(); @@ -29,8 +34,6 @@ export default function (window: Window, $: JQueryStatic) { return new BluebirdPromise(function (resolve, reject) { $.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET, {}, undefined, "json") .done(function (registrationRequest: U2f.Request) { - jslogger.debug("registrationRequest = %s", JSON.stringify(registrationRequest)); - const registerRequest: u2fApi.RegisterRequest = registrationRequest; u2fApi.register([registerRequest], [], 120) .then(function (res: u2fApi.RegisterResponse) { @@ -47,7 +50,7 @@ export default function (window: Window, $: JQueryStatic) { } function onRegisterFailure(err: Error) { - notifier.error("Problem while registering your U2F device."); + notifier.error(UserMessages.REGISTRATION_U2F_FAILED); } $(document).ready(function () { diff --git a/client/test/firstfactor/FirstFactorValidator.test.ts b/client/test/firstfactor/FirstFactorValidator.test.ts index acae7c0d7..ac8353278 100644 --- a/client/test/firstfactor/FirstFactorValidator.test.ts +++ b/client/test/firstfactor/FirstFactorValidator.test.ts @@ -38,7 +38,7 @@ describe("test FirstFactorValidator", function () { describe("should fail first factor validation", () => { it("should fail with error", () => { - return should_fail_first_factor_validation("Authetication failed. Please check your credentials."); + return should_fail_first_factor_validation("Authentication failed. Please check your credentials."); }); }); }); \ No newline at end of file diff --git a/client/test/secondfactor/TOTPValidator.test.ts b/client/test/secondfactor/TOTPValidator.test.ts index e03b0b9ad..5952eb611 100644 --- a/client/test/secondfactor/TOTPValidator.test.ts +++ b/client/test/secondfactor/TOTPValidator.test.ts @@ -5,33 +5,33 @@ import BluebirdPromise = require("bluebird"); import Assert = require("assert"); describe("test TOTPValidator", function () { - it("should initiate an identity check successfully", () => { - const postPromise = JQueryMock.JQueryDeferredMock(); - postPromise.done.yields(); - postPromise.done.returns(postPromise); + it("should initiate an identity check successfully", () => { + const postPromise = JQueryMock.JQueryDeferredMock(); + postPromise.done.yields(); + postPromise.done.returns(postPromise); - const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.jquery.ajax.returns(postPromise); + const jqueryMock = JQueryMock.JQueryMock(); + jqueryMock.jquery.ajax.returns(postPromise); - return TOTPValidator.validate("totp_token", jqueryMock.jquery as any); - }); + return TOTPValidator.validate("totp_token", jqueryMock.jquery as any); + }); - it("should fail validating TOTP token", () => { - const errorMessage = "Error while validating TOTP token"; + it("should fail validating TOTP token", () => { + const errorMessage = "Error while validating TOTP token"; - const postPromise = JQueryMock.JQueryDeferredMock(); - postPromise.fail.yields(undefined, errorMessage); - postPromise.done.returns(postPromise); + const postPromise = JQueryMock.JQueryDeferredMock(); + postPromise.fail.yields(undefined, errorMessage); + postPromise.done.returns(postPromise); - const jqueryMock = JQueryMock.JQueryMock(); - jqueryMock.jquery.ajax.returns(postPromise); + const jqueryMock = JQueryMock.JQueryMock(); + jqueryMock.jquery.ajax.returns(postPromise); - return TOTPValidator.validate("totp_token", jqueryMock.jquery as any) - .then(function () { - return BluebirdPromise.reject(new Error("Registration successfully finished while it should have not.")); - }, function (err: Error) { - Assert.equal(errorMessage, err.message); - return BluebirdPromise.resolve(); - }); - }); + return TOTPValidator.validate("totp_token", jqueryMock.jquery as any) + .then(function () { + return BluebirdPromise.reject(new Error("Registration successfully finished while it should have not.")); + }, function (err: Error) { + Assert.equal(errorMessage, err.message); + return BluebirdPromise.resolve(); + }); + }); }); \ No newline at end of file diff --git a/package.json b/package.json index 5a3e0a067..2393c6655 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "@types/query-string": "^4.3.1", "@types/randomstring": "^1.1.5", "@types/redis": "^2.6.0", - "@types/request": "0.0.46", - "@types/request-promise": "^4.1.37", + "@types/request": "^2.0.5", + "@types/request-promise": "^4.1.38", "@types/selenium-webdriver": "^3.0.4", "@types/sinon": "^2.2.1", "@types/speakeasy": "^2.0.1", @@ -96,7 +96,7 @@ "power-assert": "^1.4.4", "proxyquire": "^1.8.0", "query-string": "^4.3.4", - "request": "^2.81.0", + "request": "^2.83.0", "request-promise": "^4.2.2", "selenium-webdriver": "^3.5.0", "should": "^11.1.1", diff --git a/server/src/lib/ErrorReplies.ts b/server/src/lib/ErrorReplies.ts index 6e40e8db7..cd6a4599e 100644 --- a/server/src/lib/ErrorReplies.ts +++ b/server/src/lib/ErrorReplies.ts @@ -3,12 +3,12 @@ import BluebirdPromise = require("bluebird"); import { IRequestLogger } from "./logging/IRequestLogger"; function replyWithError(req: express.Request, res: express.Response, - code: number, logger: IRequestLogger): (err: Error) => void { + code: number, logger: IRequestLogger, body?: Object): (err: Error) => void { return function (err: Error): void { logger.error(req, "Reply with error %d: %s", code, err.message); logger.debug(req, "%s", err.stack); res.status(code); - res.send(); + res.send(body); }; } @@ -27,7 +27,7 @@ export function replyWithError403(req: express.Request, return replyWithError(req, res, 403, logger); } -export function replyWithError500(req: express.Request, - res: express.Response, logger: IRequestLogger) { - return replyWithError(req, res, 500, logger); +export function replyWithError200(req: express.Request, + res: express.Response, logger: IRequestLogger, message: string) { + return replyWithError(req, res, 200, logger, { error: message }); } \ No newline at end of file diff --git a/server/src/lib/FirstFactorValidator.ts b/server/src/lib/FirstFactorValidator.ts index 678dfde13..045d63f01 100644 --- a/server/src/lib/FirstFactorValidator.ts +++ b/server/src/lib/FirstFactorValidator.ts @@ -9,7 +9,9 @@ export function validate(req: express.Request): BluebirdPromise { return AuthenticationSession.get(req) .then(function (authSession: AuthenticationSession.AuthenticationSession) { if (!authSession.userid || !authSession.first_factor) - return BluebirdPromise.reject(new Exceptions.FirstFactorValidationError("First factor has not been validated yet.")); + return BluebirdPromise.reject( + new Exceptions.FirstFactorValidationError( + "First factor has not been validated yet.")); return BluebirdPromise.resolve(); }); diff --git a/server/src/lib/IdentityCheckMiddleware.ts b/server/src/lib/IdentityCheckMiddleware.ts index ffe9292f5..362288dc3 100644 --- a/server/src/lib/IdentityCheckMiddleware.ts +++ b/server/src/lib/IdentityCheckMiddleware.ts @@ -28,8 +28,10 @@ export interface IdentityValidable { preValidationInit(req: express.Request): BluebirdPromise; postValidationInit(req: express.Request): BluebirdPromise; - preValidationResponse(req: express.Request, res: express.Response): void; // Serves a page after identity check request - postValidationResponse(req: express.Request, res: express.Response): void; // Serves the page if identity validated + // Serves a page after identity check request + preValidationResponse(req: express.Request, res: express.Response): void; + // Serves the page if identity validated + postValidationResponse(req: express.Request, res: express.Response): void; mailSubject(): string; } @@ -50,7 +52,8 @@ function consumeToken(token: string, challenge: string, userDataStore: IUserData return userDataStore.consumeIdentityValidationToken(token, challenge); } -export function register(app: express.Application, pre_validation_endpoint: string, post_validation_endpoint: string, handler: IdentityValidable) { +export function register(app: express.Application, pre_validation_endpoint: string, + post_validation_endpoint: string, handler: IdentityValidable) { app.get(pre_validation_endpoint, get_start_validation(handler, post_validation_endpoint)); app.get(post_validation_endpoint, get_finish_validation(handler)); } @@ -91,14 +94,13 @@ export function get_finish_validation(handler: IdentityValidable): express.Reque handler.postValidationResponse(req, res); return BluebirdPromise.resolve(); }) - .catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(req, res, logger)) - .catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(req, res, logger)) - .catch(ErrorReplies.replyWithError500(req, res, logger)); + .catch(ErrorReplies.replyWithError401(req, res, logger)); }; } -export function get_start_validation(handler: IdentityValidable, postValidationEndpoint: string): express.RequestHandler { +export function get_start_validation(handler: IdentityValidable, postValidationEndpoint: string) + : express.RequestHandler { return function (req: express.Request, res: express.Response): BluebirdPromise { const logger = ServerVariablesHandler.getLogger(req.app); const notifier = ServerVariablesHandler.getNotifier(req.app); @@ -113,13 +115,15 @@ export function get_start_validation(handler: IdentityValidable, postValidationE logger.info(req, "Start identity validation of user \"%s\"", userid); if (!(email && userid)) - return BluebirdPromise.reject(new Exceptions.IdentityError("Missing user id or email address")); + return BluebirdPromise.reject(new Exceptions.IdentityError( + "Missing user id or email address")); return createAndSaveToken(userid, handler.challenge(), userDataStore); }) .then(function (token: string) { const host = req.get("Host"); - const link_url = util.format("https://%s%s?identity_token=%s", host, postValidationEndpoint, token); + const link_url = util.format("https://%s%s?identity_token=%s", host, + postValidationEndpoint, token); logger.info(req, "Notification sent to user \"%s\"", identity.userid); return notifier.notify(identity, handler.mailSubject(), link_url); }) @@ -127,9 +131,6 @@ export function get_start_validation(handler: IdentityValidable, postValidationE handler.preValidationResponse(req, res); return BluebirdPromise.resolve(); }) - .catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(req, res, logger)) - .catch(Exceptions.IdentityError, ErrorReplies.replyWithError400(req, res, logger)) - .catch(Exceptions.AccessDeniedError, ErrorReplies.replyWithError403(req, res, logger)) - .catch(ErrorReplies.replyWithError500(req, res, logger)); + .catch(ErrorReplies.replyWithError401(req, res, logger)); }; } diff --git a/server/src/lib/routes/FirstFactorBlocker.ts b/server/src/lib/routes/FirstFactorBlocker.ts index 0cab96e9f..41b88240c 100644 --- a/server/src/lib/routes/FirstFactorBlocker.ts +++ b/server/src/lib/routes/FirstFactorBlocker.ts @@ -7,6 +7,7 @@ import ErrorReplies = require("../ErrorReplies"); import objectPath = require("object-path"); import { ServerVariablesHandler } from "../ServerVariablesHandler"; import AuthenticationSession = require("../AuthenticationSession"); +import UserMessages = require("../../../../shared/UserMessages"); type Handler = (req: express.Request, res: express.Response) => BluebirdPromise; @@ -21,6 +22,6 @@ export default function (callback: Handler): Handler { .then(function () { return callback(req, res); }) - .catch(Exceptions.FirstFactorValidationError, ErrorReplies.replyWithError401(req, res, logger)); + .catch(ErrorReplies.replyWithError401(req, res, logger)); }; } \ No newline at end of file diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index b7ba04327..086700bbf 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -12,6 +12,7 @@ import { ServerVariablesHandler } from "../../ServerVariablesHandler"; import AuthenticationSession = require("../../AuthenticationSession"); import Constants = require("../../../../../shared/constants"); import { DomainExtractor } from "../../utils/DomainExtractor"; +import UserMessages = require("../../../../../shared/UserMessages"); export default function (req: express.Request, res: express.Response): BluebirdPromise { const username: string = req.body.username; @@ -22,9 +23,7 @@ export default function (req: express.Request, res: express.Response): BluebirdP const config = ServerVariablesHandler.getConfiguration(req.app); if (!username || !password) { - const err = new Error("No username or password"); - ErrorReplies.replyWithError401(req, res, logger)(err); - return BluebirdPromise.reject(err); + return BluebirdPromise.reject(new Error("No username or password.")); } const regulator = ServerVariablesHandler.getAuthenticationRegulator(req.app); @@ -93,12 +92,9 @@ export default function (req: express.Request, res: express.Response): BluebirdP } return BluebirdPromise.resolve(); }) - .catch(exceptions.LdapSearchError, ErrorReplies.replyWithError500(req, res, logger)) .catch(exceptions.LdapBindError, function (err: Error) { regulator.mark(username, false); - return ErrorReplies.replyWithError401(req, res, logger)(err); + return ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)(err); }) - .catch(exceptions.AuthenticationRegulationError, ErrorReplies.replyWithError403(req, res, logger)) - .catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError401(req, res, logger)) - .catch(ErrorReplies.replyWithError500(req, res, logger)); + .catch(ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)); } diff --git a/server/src/lib/routes/password-reset/form/post.ts b/server/src/lib/routes/password-reset/form/post.ts index d84c4c863..15dba4be2 100644 --- a/server/src/lib/routes/password-reset/form/post.ts +++ b/server/src/lib/routes/password-reset/form/post.ts @@ -6,6 +6,7 @@ import exceptions = require("../../../Exceptions"); import { ServerVariablesHandler } from "../../../ServerVariablesHandler"; import AuthenticationSession = require("../../../AuthenticationSession"); import ErrorReplies = require("../../../ErrorReplies"); +import UserMessages = require("../../../../../../shared/UserMessages"); import Constants = require("./../constants"); @@ -23,8 +24,6 @@ export default function (req: express.Request, res: express.Response): BluebirdP logger.debug(req, "Challenge %s", authSession.identity_check.challenge); if (authSession.identity_check.challenge != Constants.CHALLENGE) { - res.status(403); - res.send(); return BluebirdPromise.reject(new Error("Bad challenge.")); } return ldapPasswordUpdater.updatePassword(authSession.identity_check.userid, newPassword); @@ -37,5 +36,5 @@ export default function (req: express.Request, res: express.Response): BluebirdP res.send(); return BluebirdPromise.resolve(); }) - .catch(ErrorReplies.replyWithError500(req, res, logger)); + .catch(ErrorReplies.replyWithError200(req, res, logger, UserMessages.RESET_PASSWORD_FAILED)); } diff --git a/server/src/lib/routes/secondfactor/redirect.ts b/server/src/lib/routes/secondfactor/redirect.ts index 4761f2a94..4f258e8ab 100644 --- a/server/src/lib/routes/secondfactor/redirect.ts +++ b/server/src/lib/routes/secondfactor/redirect.ts @@ -18,5 +18,6 @@ export default function (req: express.Request, res: express.Response): BluebirdP }); return BluebirdPromise.resolve(); }) - .catch(ErrorReplies.replyWithError500(req, res, logger)); + .catch(ErrorReplies.replyWithError200(req, res, logger, + "Unexpected error.")); } \ No newline at end of file diff --git a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts index a395bf132..d289773fe 100644 --- a/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts +++ b/server/src/lib/routes/secondfactor/totp/identity/RegistrationHandler.ts @@ -11,6 +11,7 @@ import Endpoints = require("../../../../../../../shared/api"); import ErrorReplies = require("../../../../ErrorReplies"); import { ServerVariablesHandler } from "../../../../ServerVariablesHandler"; import AuthenticationSession = require("../../../../AuthenticationSession"); +import UserMessages = require("../../../../../../../shared/UserMessages"); import FirstFactorValidator = require("../../../../FirstFactorValidator"); @@ -62,8 +63,6 @@ export default class RegistrationHandler implements IdentityValidable { const challenge = authSession.identity_check.challenge; if (challenge != Constants.CHALLENGE || !userid) { - res.status(403); - res.send(); return BluebirdPromise.reject(new Error("Bad challenge.")); } @@ -83,7 +82,7 @@ export default class RegistrationHandler implements IdentityValidable { }); }); }) - .catch(ErrorReplies.replyWithError500(req, res, logger)); + .catch(ErrorReplies.replyWithError200(req, res, logger, UserMessages.OPERATION_FAILED)); } mailSubject(): string { diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.ts b/server/src/lib/routes/secondfactor/totp/sign/post.ts index 134de6533..d9b5afd9f 100644 --- a/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ b/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -10,6 +10,7 @@ import redirect from "../../redirect"; import ErrorReplies = require("../../../../ErrorReplies"); import { ServerVariablesHandler } from "./../../../../ServerVariablesHandler"; import AuthenticationSession = require("../../../../AuthenticationSession"); +import UserMessages = require("../../../../../../../shared/UserMessages"); const UNAUTHORIZED_MESSAGE = "Unauthorized access"; @@ -25,7 +26,7 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr return AuthenticationSession.get(req) .then(function (_authSession: AuthenticationSession.AuthenticationSession) { authSession = _authSession; - logger.info(req, "Initiate TOTP validation for user '%s'", authSession.userid); + logger.info(req, "Initiate TOTP validation for user '%s'.", authSession.userid); return userDataStore.retrieveTOTPSecret(authSession.userid); }) .then(function (doc: TOTPSecretDocument) { @@ -33,11 +34,11 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr return totpValidator.validate(token, doc.secret.base32); }) .then(function () { - logger.debug(req, "TOTP validation succeeded"); + logger.debug(req, "TOTP validation succeeded."); authSession.second_factor = true; redirect(req, res); return BluebirdPromise.resolve(); }) - .catch(exceptions.InvalidTOTPError, ErrorReplies.replyWithError401(req, res, logger)) - .catch(ErrorReplies.replyWithError500(req, res, logger)); + .catch(ErrorReplies.replyWithError200(req, res, logger, + UserMessages.OPERATION_FAILED)); } diff --git a/server/src/lib/routes/secondfactor/u2f/register/post.ts b/server/src/lib/routes/secondfactor/u2f/register/post.ts index 1a64ad64d..6d7e9d4b1 100644 --- a/server/src/lib/routes/secondfactor/u2f/register/post.ts +++ b/server/src/lib/routes/secondfactor/u2f/register/post.ts @@ -12,6 +12,7 @@ import redirect from "../../redirect"; import ErrorReplies = require("../../../../ErrorReplies"); import { ServerVariablesHandler } from "../../../../ServerVariablesHandler"; import AuthenticationSession = require("../../../../AuthenticationSession"); +import UserMessages = require("../../../../../../../shared/UserMessages"); export default FirstFactorBlocker(handler); @@ -31,15 +32,11 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise 403 + .catch(exceptions.DomainAccessDenied, ErrorReplies + .replyWithError403(req, res, logger)) + // The user is not yet authenticated -> 401 .catch(ErrorReplies.replyWithError401(req, res, logger)); } diff --git a/server/src/views/layout/layout.pug b/server/src/views/layout/layout.pug index 3f95cb9bc..24d96080a 100644 --- a/server/src/views/layout/layout.pug +++ b/server/src/views/layout/layout.pug @@ -5,7 +5,7 @@ html title Authelia - 2FA meta(name="viewport", content="width=device-width, initial-scale=1.0")/ link(rel="icon", href="/img/icon.png" type="image/png" sizes="32x32")/ - link(rel="stylesheet", type="text/css", href="/css/authelia.min.css")/ + link(rel="stylesheet", type="text/css", href="/css/authelia.css")/ if redirection_url body diff --git a/server/test/IdentityCheckMiddleware.test.ts b/server/test/IdentityCheckMiddleware.test.ts index 37388c3e6..62416179b 100644 --- a/server/test/IdentityCheckMiddleware.test.ts +++ b/server/test/IdentityCheckMiddleware.test.ts @@ -5,7 +5,7 @@ import AuthenticationSession = require("../src/lib/AuthenticationSession"); import { UserDataStore } from "../src/lib/storage/UserDataStore"; import exceptions = require("../src/lib/Exceptions"); -import assert = require("assert"); +import Assert = require("assert"); import Promise = require("bluebird"); import express = require("express"); import BluebirdPromise = require("bluebird"); @@ -69,11 +69,11 @@ describe("test identity check process", function () { return callback(req as any, res as any, undefined) .then(function () { return BluebirdPromise.reject("Should fail"); }) .catch(function () { - assert.equal(res.status.getCall(0).args[0], 401); + Assert.equal(res.status.getCall(0).args[0], 401); }); }); - it("should send 400 if email is missing in provided identity", function () { + it("should send 401 if email is missing in provided identity", function () { const identity = { userid: "abc" }; identityValidable.preValidationInit.returns(BluebirdPromise.resolve(identity)); @@ -82,11 +82,11 @@ describe("test identity check process", function () { return callback(req as any, res as any, undefined) .then(function () { return BluebirdPromise.reject("Should fail"); }) .catch(function () { - assert.equal(res.status.getCall(0).args[0], 400); + Assert.equal(res.status.getCall(0).args[0], 401); }); }); - it("should send 400 if userid is missing in provided identity", function () { + it("should send 401 if userid is missing in provided identity", function () { const endpoint = "/protected"; const identity = { email: "abc@example.com" }; @@ -96,7 +96,7 @@ describe("test identity check process", function () { return callback(req as any, res as any, undefined) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .catch(function (err: Error) { - assert.equal(res.status.getCall(0).args[0], 400); + Assert.equal(res.status.getCall(0).args[0], 401); return BluebirdPromise.resolve(); }); }); @@ -111,23 +111,23 @@ describe("test identity check process", function () { return callback(req as any, res as any, undefined) .then(function () { - assert(notifier.notify.calledOnce); - assert(mocks.userDataStore.produceIdentityValidationTokenStub.calledOnce); - assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub.getCall(0).args[0], "user"); - assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub.getCall(0).args[3], 240000); + Assert(notifier.notify.calledOnce); + Assert(mocks.userDataStore.produceIdentityValidationTokenStub.calledOnce); + Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub.getCall(0).args[0], "user"); + Assert.equal(mocks.userDataStore.produceIdentityValidationTokenStub.getCall(0).args[3], 240000); }); }); } function test_finish_get_handler() { - it("should send 403 if no identity_token is provided", function () { + it("should send 401 if no identity_token is provided", function () { const callback = IdentityValidator.get_finish_validation(identityValidable); return callback(req as any, res as any, undefined) .then(function () { return BluebirdPromise.reject("Should fail"); }) .catch(function () { - assert.equal(res.status.getCall(0).args[0], 403); + Assert.equal(res.status.getCall(0).args[0], 401); }); }); @@ -138,7 +138,7 @@ describe("test identity check process", function () { return callback(req as any, res as any, undefined); }); - it("should return 500 if identity_token is provided but invalid", function () { + it("should return 401 if identity_token is provided but invalid", function () { req.query.identity_token = "token"; mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.reject(new Error("Invalid token"))); @@ -147,7 +147,7 @@ describe("test identity check process", function () { return callback(req as any, res as any, undefined) .then(function () { return BluebirdPromise.reject("Should fail"); }) .catch(function () { - assert.equal(res.status.getCall(0).args[0], 500); + Assert.equal(res.status.getCall(0).args[0], 401); }); }); @@ -165,7 +165,7 @@ describe("test identity check process", function () { }) .then(function () { return BluebirdPromise.reject("Should fail"); }) .catch(function () { - assert.equal(authSession.identity_check.userid, "user"); + Assert.equal(authSession.identity_check.userid, "user"); return BluebirdPromise.resolve(); }); }); diff --git a/server/test/routes/firstfactor/post.test.ts b/server/test/routes/firstfactor/post.test.ts index 17b3548c9..89c1d72e8 100644 --- a/server/test/routes/firstfactor/post.test.ts +++ b/server/test/routes/firstfactor/post.test.ts @@ -1,7 +1,7 @@ import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); -import assert = require("assert"); +import Assert = require("assert"); import winston = require("winston"); import FirstFactorPost = require("../../../src/lib/routes/firstfactor/post"); @@ -87,8 +87,8 @@ describe("test the first factor validation route", function () { return FirstFactorPost.default(req as any, res as any); }) .then(function () { - assert.equal("username", authSession.userid); - assert(res.send.calledOnce); + Assert.equal("username", authSession.userid); + Assert(res.send.calledOnce); }); }); @@ -113,27 +113,32 @@ describe("test the first factor validation route", function () { return FirstFactorPost.default(req as any, res as any); }) .then(function () { - assert.equal("test_ok@example.com", authSession.email); + Assert.equal("test_ok@example.com", authSession.email); }); }); - it("should return status code 401 when LDAP authenticator throws", function () { + it("should return error message when LDAP authenticator throws", function () { (serverVariables.ldapAuthenticator as any).authenticate.withArgs("username", "password") .returns(BluebirdPromise.reject(new exceptions.LdapBindError("Bad credentials"))); return FirstFactorPost.default(req as any, res as any) .then(function () { - assert.equal(401, res.status.getCall(0).args[0]); - assert.equal(regulator.mark.getCall(0).args[0], "username"); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.equal(regulator.mark.getCall(0).args[0], "username"); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); }); }); - it("should return status code 403 when regulator rejects authentication", function () { + it("should return error message when regulator rejects authentication", function () { const err = new exceptions.AuthenticationRegulationError("Authentication regulation..."); regulator.regulate.returns(BluebirdPromise.reject(err)); return FirstFactorPost.default(req as any, res as any) .then(function () { - assert.equal(403, res.status.getCall(0).args[0]); - assert.equal(1, res.send.callCount); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); }); }); }); diff --git a/server/test/routes/password-reset/post.test.ts b/server/test/routes/password-reset/post.test.ts index 86fda49cd..436715ad0 100644 --- a/server/test/routes/password-reset/post.test.ts +++ b/server/test/routes/password-reset/post.test.ts @@ -6,7 +6,7 @@ import { ServerVariablesHandler } from "../../../src/lib/ServerVariablesHandler" import { UserDataStore } from "../../../src/lib/storage/UserDataStore"; import Sinon = require("sinon"); import winston = require("winston"); -import assert = require("assert"); +import Assert = require("assert"); import BluebirdPromise = require("bluebird"); import ExpressMock = require("../../mocks/express"); @@ -85,9 +85,9 @@ describe("test reset password route", function () { .then(function () { return AuthenticationSession.get(req as any); }).then(function (_authSession: AuthenticationSession.AuthenticationSession) { - assert.equal(res.status.getCall(0).args[0], 204); - assert.equal(_authSession.first_factor, false); - assert.equal(_authSession.second_factor, false); + Assert.equal(res.status.getCall(0).args[0], 204); + Assert.equal(_authSession.first_factor, false); + Assert.equal(_authSession.second_factor, false); return BluebirdPromise.resolve(); }); }); @@ -102,7 +102,10 @@ describe("test reset password route", function () { return PasswordResetFormPost.default(req as any, res as any); }) .then(function () { - assert.equal(res.status.getCall(0).args[0], 403); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); }); }); @@ -121,7 +124,10 @@ describe("test reset password route", function () { }; return PasswordResetFormPost.default(req as any, res as any); }).then(function () { - assert.equal(res.status.getCall(0).args[0], 500); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "An error occurred during password reset. Your password has not been changed." + }); return BluebirdPromise.resolve(); }); }); diff --git a/server/test/routes/secondfactor/u2f/register/post.test.ts b/server/test/routes/secondfactor/u2f/register/post.test.ts index 601e7f099..86d0cd01d 100644 --- a/server/test/routes/secondfactor/u2f/register/post.test.ts +++ b/server/test/routes/secondfactor/u2f/register/post.test.ts @@ -84,7 +84,7 @@ describe("test u2f routes: register", function () { }); }); - it("should return unauthorized on finishRegistration error", function () { + it("should return error message on finishRegistration error", function () { const user_key_container = {}; const u2f_mock = U2FMock.U2FMock(); u2f_mock.checkRegistration.returns({ errorCode: 500 }); @@ -103,12 +103,15 @@ describe("test u2f routes: register", function () { }) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .catch(function () { - assert.equal(500, res.status.getCall(0).args[0]); + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); return BluebirdPromise.resolve(); }); }); - it("should return 403 when register_request is not provided", function () { + it("should return error message when register_request is not provided", function () { const user_key_container = {}; const u2f_mock = U2FMock.U2FMock(); u2f_mock.checkRegistration.returns(BluebirdPromise.resolve()); @@ -121,12 +124,15 @@ describe("test u2f routes: register", function () { }) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .catch(function () { - assert.equal(403, res.status.getCall(0).args[0]); + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); return BluebirdPromise.resolve(); }); }); - it("should return forbidden error when no auth request has been initiated", function () { + it("should return error message when no auth request has been initiated", function () { const user_key_container = {}; const u2f_mock = U2FMock.U2FMock(); u2f_mock.checkRegistration.returns(BluebirdPromise.resolve()); @@ -139,12 +145,15 @@ describe("test u2f routes: register", function () { }) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .catch(function () { - assert.equal(403, res.status.getCall(0).args[0]); + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); return BluebirdPromise.resolve(); }); }); - it("should return forbidden error when identity has not been verified", function () { + it("should return error message when identity has not been verified", function () { return AuthenticationSession.get(req as any) .then(function (authSession) { authSession.identity_check = undefined; @@ -152,7 +161,10 @@ describe("test u2f routes: register", function () { }) .then(function () { return BluebirdPromise.reject(new Error("It should fail")); }) .catch(function () { - assert.equal(403, res.status.getCall(0).args[0]); + assert.equal(200, res.status.getCall(0).args[0]); + assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); return BluebirdPromise.resolve(); }); }); diff --git a/server/test/routes/secondfactor/u2f/register_request/get.test.ts b/server/test/routes/secondfactor/u2f/register_request/get.test.ts index 7465beb1e..168dca30d 100644 --- a/server/test/routes/secondfactor/u2f/register_request/get.test.ts +++ b/server/test/routes/secondfactor/u2f/register_request/get.test.ts @@ -1,7 +1,7 @@ import sinon = require("sinon"); import BluebirdPromise = require("bluebird"); -import assert = require("assert"); +import Assert = require("assert"); import U2FRegisterRequestGet = require("../../../../../src/lib/routes/secondfactor/u2f/register_request/get"); import AuthenticationSession = require("../../../../../src/lib/AuthenticationSession"); import { ServerVariablesHandler } from "../../../../../src/lib/ServerVariablesHandler"; @@ -67,28 +67,31 @@ describe("test u2f routes: register_request", function () { mocks.u2f = u2f_mock; return U2FRegisterRequestGet.default(req as any, res as any) .then(function () { - assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); + Assert.deepEqual(expectedRequest, res.json.getCall(0).args[0]); }); }); - it("should return internal error on registration request", function (done) { - res.send = sinon.spy(function (data: any) { - assert.equal(500, res.status.getCall(0).args[0]); - done(); - }); + it("should return internal error on registration request", function () { + res.send = sinon.spy(); const user_key_container = {}; const u2f_mock = U2FMock.U2FMock(); u2f_mock.request.returns(BluebirdPromise.reject("Internal error")); mocks.u2f = u2f_mock; - U2FRegisterRequestGet.default(req as any, res as any); + return U2FRegisterRequestGet.default(req as any, res as any) + .then(function() { + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], { + error: "Operation failed." + }); + }); }); it("should return forbidden if identity has not been verified", function () { authSession.identity_check = undefined; return U2FRegisterRequestGet.default(req as any, res as any) .then(function () { - assert.equal(403, res.status.getCall(0).args[0]); + Assert.equal(403, res.status.getCall(0).args[0]); }); }); }); diff --git a/server/test/routes/secondfactor/u2f/sign/post.test.ts b/server/test/routes/secondfactor/u2f/sign/post.test.ts index 80d7394ea..aef978bbc 100644 --- a/server/test/routes/secondfactor/u2f/sign/post.test.ts +++ b/server/test/routes/secondfactor/u2f/sign/post.test.ts @@ -97,7 +97,9 @@ describe("test u2f routes: sign", function () { mocks.u2f = u2f_mock; return U2FSignPost.default(req as any, res as any) .then(function () { - Assert.equal(500, res.status.getCall(0).args[0]); + Assert.equal(res.status.getCall(0).args[0], 200); + Assert.deepEqual(res.send.getCall(0).args[0], + { error: "Operation failed." }); }); }); }); diff --git a/server/test/server/PrivatePages.ts b/server/test/server/PrivatePages.ts index 09bdb383b..cb2d8372b 100644 --- a/server/test/server/PrivatePages.ts +++ b/server/test/server/PrivatePages.ts @@ -127,25 +127,18 @@ describe("Private pages of the server must not be accessible without session", f }); describe("Second factor endpoints must be protected if first factor is not validated", function () { - function should_post_and_reply_with(url: string, status_code: number): BluebirdPromise { - return requestp.postAsync(url).then(function (response: request.RequestResponse) { - Assert.equal(response.statusCode, status_code); - return BluebirdPromise.resolve(); - }); - } - - function should_get_and_reply_with(url: string, status_code: number): BluebirdPromise { - return requestp.getAsync(url).then(function (response: request.RequestResponse) { - Assert.equal(response.statusCode, status_code); - return BluebirdPromise.resolve(); - }); - } - function should_post_and_reply_with_401(url: string): BluebirdPromise { - return should_post_and_reply_with(url, 401); + return requestp.postAsync(url).then(function (response: request.RequestResponse) { + Assert.equal(response.statusCode, 401); + return BluebirdPromise.resolve(); + }); } + function should_get_and_reply_with_401(url: string): BluebirdPromise { - return should_get_and_reply_with(url, 401); + return requestp.getAsync(url).then(function (response: request.RequestResponse) { + Assert.equal(response.statusCode, 401); + return BluebirdPromise.resolve(); + }); } it("should block " + Endpoints.SECOND_FACTOR_GET, function () { diff --git a/shared/UserMessages.ts b/shared/UserMessages.ts new file mode 100644 index 000000000..40640b1d6 --- /dev/null +++ b/shared/UserMessages.ts @@ -0,0 +1,23 @@ + +export const AUTHENTICATION_FAILED = "Authentication failed. Please check your credentials."; +export const AUTHENTICATION_SUCCEEDED = "Authentication succeeded. You can now access your services."; + +export const AUTHENTICATION_U2F_FAILED = "Authentication failed. Have you already registered your device?"; +export const AUTHENTICATION_TOTP_FAILED = "Authentication failed. Have you already registered your secret?"; + +export const U2F_TRANSACTION_FINISH_FAILED = "U2F validation failed unexpectedly."; +export const PLEASE_TOUCH_TOKEN = "Please touch the token on your U2F device."; + +export const REGISTRATION_U2F_FAILED = "Registration of U2F device failed."; + +export const DIFFERENT_PASSWORDS = "The passwords are different."; +export const MISSING_PASSWORD = "You must enter your password twice."; +export const RESET_PASSWORD_FAILED = "An error occurred during password reset. Your password has not been changed."; + +// Password reset request +export const MISSING_USERNAME = "You must provide your username to reset your password."; +export const MAIL_SENT = "An email has been sent to you. Follow the link to change your password."; +export const MAIL_NOT_SENT = "The email cannot be sent. Please retry in few minutes."; + +export const UNAUTHORIZED_OPERATION = "You are not allowed to perform this operation."; +export const OPERATION_FAILED = "Operation failed."; \ No newline at end of file diff --git a/test/features/authentication.feature b/test/features/authentication.feature index 69ea88843..11a600fcb 100644 --- a/test/features/authentication.feature +++ b/test/features/authentication.feature @@ -12,7 +12,7 @@ Feature: Authentication scenarii 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 double check your credentials." + 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://auth.test.local:8080/" @@ -31,7 +31,7 @@ Feature: Authentication scenarii And I login with user "john" and password "password" And I use "BADTOKEN" as TOTP token And I click on "TOTP" - Then I get a notification of type "error" with message "Problem with TOTP validation." + 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://auth.test.local:8080/logout?redirect=https://home.test.local:8080/" diff --git a/test/features/regulation.feature b/test/features/regulation.feature index f361c2dd5..69e5a69ba 100644 --- a/test/features/regulation.feature +++ b/test/features/regulation.feature @@ -7,16 +7,16 @@ Feature: Authelia regulates authentication to avoid brute force And I set field "username" to "blackhat" And I set field "password" to "bad-password" And I click on "Sign in" - And I get a notification of type "error" with message "Authentication failed. Please double check your credentials." + And I get a notification of type "error" with message "Authentication failed. Please check your credentials." And I set field "password" to "bad-password" And I click on "Sign in" - And I get a notification of type "error" with message "Authentication failed. Please double check your credentials." + And I get a notification of type "error" with message "Authentication failed. Please check your credentials." And I set field "password" to "bad-password" And I click on "Sign in" - And I get a notification of type "error" with message "Authentication failed. Please double check your credentials." + And I get a notification of type "error" with message "Authentication failed. Please check your credentials." When I set field "password" to "password" And I click on "Sign in" - Then I get a notification of type "error" with message "Authentication failed. Please double check your credentials." + Then I get a notification of type "error" with message "Authentication failed. Please check your credentials." @needs-test-config @need-registered-user-blackhat @@ -25,13 +25,13 @@ Feature: Authelia regulates authentication to avoid brute force And I set field "username" to "blackhat" And I set field "password" to "bad-password" And I click on "Sign in" - And I get a notification of type "error" with message "Authentication failed. Please double check your credentials." + And I get a notification of type "error" with message "Authentication failed. Please check your credentials." And I set field "password" to "bad-password" And I click on "Sign in" - And I get a notification of type "error" with message "Authentication failed. Please double check your credentials." + And I get a notification of type "error" with message "Authentication failed. Please check your credentials." And I set field "password" to "bad-password" And I click on "Sign in" - And I get a notification of type "error" with message "Authentication failed. Please double check your credentials." + And I get a notification of type "error" with message "Authentication failed. Please check your credentials." When I wait 6 seconds And I set field "password" to "password" And I click on "Sign in" diff --git a/test/features/reset-password.feature b/test/features/reset-password.feature index c386f28c7..ba893f655 100644 --- a/test/features/reset-password.feature +++ b/test/features/reset-password.feature @@ -11,6 +11,12 @@ Feature: User is able to reset his password 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://auth.test.local: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://auth.test.local:8080/password-reset/request And I set field "username" to "james" diff --git a/test/features/restrictions.feature b/test/features/restrictions.feature index c2cf9889d..e331482b9 100644 --- a/test/features/restrictions.feature +++ b/test/features/restrictions.feature @@ -9,8 +9,8 @@ Feature: Non authenticated users have no access to certain pages | https://auth.test.local:8080/secondfactor | 401 | | https://auth.test.local:8080/verify | 401 | | https://auth.test.local:8080/secondfactor/u2f/identity/start | 401 | - | https://auth.test.local:8080/secondfactor/u2f/identity/finish | 403 | + | https://auth.test.local:8080/secondfactor/u2f/identity/finish | 401 | | https://auth.test.local:8080/secondfactor/totp/identity/start | 401 | - | https://auth.test.local:8080/secondfactor/totp/identity/finish | 403 | - | https://auth.test.local:8080/password-reset/identity/start | 403 | - | https://auth.test.local:8080/password-reset/identity/finish | 403 | \ No newline at end of file + | https://auth.test.local:8080/secondfactor/totp/identity/finish | 401 | + | https://auth.test.local:8080/password-reset/identity/start | 401 | + | https://auth.test.local:8080/password-reset/identity/finish | 401 | \ No newline at end of file