Merge pull request #140 from clems4ever/improve-endpoint-errors

Every public endpoints return 200 with harmonized error messages or 401
This commit is contained in:
Clément Michaud 2017-10-14 12:22:24 +02:00 committed by GitHub
commit f041b946d9
39 changed files with 325 additions and 249 deletions

View File

@ -59,7 +59,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: {

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
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";
@ -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;
}
$(document).ready(function () {
jslogger.debug("Reset password request form setup");
$(Constants.FORM_SELECTOR).on("submit", onFormSubmitted);
});
}

View File

@ -12,8 +12,12 @@ export function validate(token: string, $: JQueryStatic): BluebirdPromise<string
method: "POST",
dataType: "json"
} as JQueryAjaxSettings)
.done(function (data: any) {
resolve(data);
.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));

View File

@ -4,6 +4,7 @@ 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<void> {
@ -14,8 +15,12 @@ function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQuerySta
method: "POST",
dataType: "json"
} as JQueryAjaxSettings)
.done(function (data) {
resolve(data);
.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));
@ -27,7 +32,7 @@ function startU2fAuthentication($: JQueryStatic, notifier: INotifier, u2fApi: ty
return new BluebirdPromise<void>(function (resolve, reject) {
$.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, undefined, "json")
.done(function (signResponse: SignMessage) {
notifier.info("Please touch the token");
notifier.info(UserMessages.PLEASE_TOUCH_TOKEN);
const signRequest: U2fApi.SignRequest = {
appId: signResponse.request.appId,
@ -42,7 +47,7 @@ function startU2fAuthentication($: JQueryStatic, notifier: INotifier, u2fApi: ty
.then(function (data) {
resolve(data);
}, function (err) {
notifier.error("Error when finish U2F transaction");
notifier.error(UserMessages.U2F_TRANSACTION_FINISH_FAILED);
reject(err);
});
})

View File

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

View File

@ -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<string>(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<string>(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 () {

View File

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

View File

@ -67,8 +67,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",
@ -97,7 +97,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",

View File

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

View File

@ -9,7 +9,9 @@ export function validate(req: express.Request): BluebirdPromise<void> {
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();
});

View File

@ -28,8 +28,10 @@ export interface IdentityValidable {
preValidationInit(req: express.Request): BluebirdPromise<Identity.Identity>;
postValidationInit(req: express.Request): BluebirdPromise<void>;
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<void> {
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.email, 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));
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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<v
const registrationRequest = authSession.register_request;
if (!registrationRequest) {
res.status(403);
res.send();
return BluebirdPromise.reject(new Error("No registration request"));
}
if (!authSession.identity_check
|| authSession.identity_check.challenge != "u2f-register") {
res.status(403);
res.send();
return BluebirdPromise.reject(new Error("Bad challenge for registration request"));
}
@ -67,5 +64,6 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise<v
redirect(req, res);
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError500(req, res, logger));
.catch(ErrorReplies.replyWithError200(req, res, logger,
UserMessages.OPERATION_FAILED));
}

View File

@ -10,6 +10,7 @@ import FirstFactorBlocker from "../../../FirstFactorBlocker";
import ErrorReplies = require("../../../../ErrorReplies");
import {  ServerVariablesHandler } from "../../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../../AuthenticationSession");
import UserMessages = require("../../../../../../../shared/UserMessages");
export default FirstFactorBlocker(handler);
@ -41,5 +42,6 @@ function handler(req: express.Request, res: express.Response): BluebirdPromise<v
res.json(registrationRequest);
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError500(req, res, logger));
.catch(ErrorReplies.replyWithError200(req, res, logger,
UserMessages.OPERATION_FAILED));
}

View File

@ -13,6 +13,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);
@ -50,6 +51,7 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr
redirect(req, res);
return BluebirdPromise.resolve();
})
.catch(ErrorReplies.replyWithError500(req, res, logger));
.catch(ErrorReplies.replyWithError200(req, res, logger,
UserMessages.OPERATION_FAILED));
}

View File

@ -13,6 +13,7 @@ import FirstFactorBlocker from "../../../FirstFactorBlocker";
import ErrorReplies = require("../../../../ErrorReplies");
import { ServerVariablesHandler } from "../../../../ServerVariablesHandler";
import AuthenticationSession = require("../../../../AuthenticationSession");
import UserMessages = require("../../../../../../../shared/UserMessages");
export default FirstFactorBlocker(handler);
@ -50,7 +51,7 @@ export function handler(req: express.Request, res: express.Response): BluebirdPr
res.json(authenticationMessage);
return BluebirdPromise.resolve();
})
.catch(exceptions.AccessDeniedError, ErrorReplies.replyWithError401(req, res, logger))
.catch(ErrorReplies.replyWithError500(req, res, logger));
.catch(ErrorReplies.replyWithError200(req, res, logger,
UserMessages.OPERATION_FAILED));
}

View File

@ -67,7 +67,10 @@ export default function (req: express.Request, res: express.Response): BluebirdP
res.send();
return BluebirdPromise.resolve();
})
.catch(exceptions.DomainAccessDenied, ErrorReplies.replyWithError403(req, res, logger))
// The user is authenticated but has restricted access -> 403
.catch(exceptions.DomainAccessDenied, ErrorReplies
.replyWithError403(req, res, logger))
// The user is not yet authenticated -> 401
.catch(ErrorReplies.replyWithError401(req, res, logger));
}

View File

@ -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
<meta http-equiv="refresh" content="5;url=#{redirection_url}">
body

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -128,25 +128,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<void> {
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<void> {
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<void> {
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<void> {
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 () {

23
shared/UserMessages.ts Normal file
View File

@ -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.";

View File

@ -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/"

View File

@ -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"

View File

@ -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"

View File

@ -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 |
| 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 |