Merge pull request #74 from clems4ever/client-notifications

Notifications to users do not use notifyjs anymore. They are more com…
pull/80/head
Clément Michaud 2017-09-02 16:53:53 +02:00 committed by GitHub
commit 9403326226
39 changed files with 425 additions and 245 deletions

View File

@ -162,7 +162,7 @@ module.exports = function (grunt) {
grunt.registerTask('docker-restart', ['run:docker-restart']); grunt.registerTask('docker-restart', ['run:docker-restart']);
grunt.registerTask('unit-tests', ['run:unit-tests']); grunt.registerTask('unit-tests', ['run:unit-tests']);
grunt.registerTask('integration-tests', ['run:unit-tests']); grunt.registerTask('integration-tests', ['run:integration-tests']);
grunt.registerTask('test', ['unit-tests']); grunt.registerTask('test', ['unit-tests']);
}; };

View File

@ -88,7 +88,6 @@
"jsdom": "^11.0.0", "jsdom": "^11.0.0",
"mocha": "^3.4.2", "mocha": "^3.4.2",
"mockdate": "^2.0.1", "mockdate": "^2.0.1",
"notifyjs-browser": "^0.4.2",
"nyc": "^10.3.2", "nyc": "^10.3.2",
"power-assert": "^1.4.4", "power-assert": "^1.4.4",
"proxyquire": "^1.8.0", "proxyquire": "^1.8.0",

View File

@ -20,3 +20,42 @@ body {
font-size: 0.7em; font-size: 0.7em;
color: #6b6b6b; color: #6b6b6b;
} }
/* notifications */
.notification {
padding: 10px;
border-radius: 6px;
display: none;
}
.notification img {
width: 24px;
margin-right: 10px;
}
.notification i,
.notification span {
display:table-cell;
vertical-align:middle;
}
.info {
border: 1px solid #9cb1ff;
background-color: rgb(192, 220, 255);
}
.success {
border: 1px solid #65ec7c;
background-color: rgb(163, 255, 157);
}
.error {
border: 1px solid #ffa3a3;
background-color: rgb(255, 175, 175);
}
.warning {
border: 1px solid #ffd743;
background-color: rgb(255, 230, 143);
}

View File

@ -1,12 +1,12 @@
import FirstFactorValidator = require("./firstfactor/FirstFactorValidator"); import FirstFactorValidator = require("./lib/firstfactor/FirstFactorValidator");
import FirstFactor from "./firstfactor/index"; import FirstFactor from "./lib/firstfactor/index";
import SecondFactor from "./secondfactor/index"; import SecondFactor from "./lib/secondfactor/index";
import TOTPRegister from "./totp-register/totp-register"; import TOTPRegister from "./lib/totp-register/totp-register";
import U2fRegister from "./u2f-register/u2f-register"; import U2fRegister from "./lib/u2f-register/u2f-register";
import ResetPasswordRequest from "./reset-password/reset-password-request"; import ResetPasswordRequest from "./lib/reset-password/reset-password-request";
import ResetPasswordForm from "./reset-password/reset-password-form"; import ResetPasswordForm from "./lib/reset-password/reset-password-form";
import jslogger = require("js-logger"); import jslogger = require("js-logger");
import jQuery = require("jquery"); import jQuery = require("jquery");
import u2fApi = require("u2f-api"); import u2fApi = require("u2f-api");
@ -14,8 +14,6 @@ import u2fApi = require("u2f-api");
jslogger.useDefaults(); jslogger.useDefaults();
jslogger.setLevel(jslogger.INFO); jslogger.setLevel(jslogger.INFO);
require("notifyjs-browser")(jQuery);
export = { export = {
firstfactor: function () { firstfactor: function () {
FirstFactor(window, jQuery, FirstFactorValidator, jslogger); FirstFactor(window, jQuery, FirstFactorValidator, jslogger);

View File

@ -0,0 +1,14 @@
declare type Handler = () => void;
export interface Handlers {
onFadedIn: Handler;
onFadedOut: Handler;
}
export interface INotifier {
success(msg: string, handlers?: Handlers): void;
error(msg: string, handlers?: Handlers): void;
warning(msg: string, handlers?: Handlers): void;
info(msg: string, handlers?: Handlers): void;
}

View File

@ -0,0 +1,45 @@
import util = require("util");
import { INotifier, Handlers } from "./INotifier";
export class Notifier implements INotifier {
private element: JQuery;
constructor(selector: string, $: JQueryStatic) {
this.element = $(selector);
}
private displayAndFadeout(msg: string, statusType: string, handlers?: Handlers): void {
const that = this;
const FADE_TIME = 500;
const html = util.format('<i><img src="/img/notifications/%s.png" alt="status %s"/></i>\
<span>%s</span>', statusType, statusType, msg);
this.element.html(html);
this.element.addClass(statusType);
this.element.fadeIn(FADE_TIME, function() {
handlers.onFadedIn();
})
.delay(4000)
.fadeOut(FADE_TIME, function() {
that.element.removeClass(statusType);
handlers.onFadedOut();
});
}
success(msg: string, handlers?: Handlers) {
this.displayAndFadeout(msg, "success", handlers);
}
error(msg: string, handlers?: Handlers) {
this.displayAndFadeout(msg, "error", handlers);
}
warning(msg: string, handlers?: Handlers) {
this.displayAndFadeout(msg, "warning", handlers);
}
info(msg: string, handlers?: Handlers) {
this.displayAndFadeout(msg, "info", handlers);
}
}

View File

@ -1,6 +1,6 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Endpoints = require("../../server/endpoints"); import Endpoints = require("../../../server/endpoints");
export function validate(username: string, password: string, $: JQueryStatic): BluebirdPromise<void> { export function validate(username: string, password: string, $: JQueryStatic): BluebirdPromise<void> {
return new BluebirdPromise<void>(function (resolve, reject) { return new BluebirdPromise<void>(function (resolve, reject) {
@ -12,9 +12,7 @@ export function validate(username: string, password: string, $: JQueryStatic): B
resolve(); resolve();
}) })
.fail(function (xhr: JQueryXHR, textStatus: string) { .fail(function (xhr: JQueryXHR, textStatus: string) {
if (xhr.status == 401)
reject(new Error("Authetication failed. Please check your credentials.")); reject(new Error("Authetication failed. Please check your credentials."));
reject(new Error(textStatus));
}); });
}); });
} }

View File

@ -1,10 +1,15 @@
import FirstFactorValidator = require("./FirstFactorValidator"); import FirstFactorValidator = require("./FirstFactorValidator");
import JSLogger = require("js-logger"); import JSLogger = require("js-logger");
import UISelectors = require("./UISelectors"); import UISelectors = require("./UISelectors");
import { Notifier } from "../Notifier";
import Endpoints = require("../../server/endpoints"); import Endpoints = require("../../../server/endpoints");
export default function (window: Window, $: JQueryStatic,
firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) {
const notifier = new Notifier(".notification", $);
export default function (window: Window, $: JQueryStatic, firstFactorValidator: typeof FirstFactorValidator, jslogger: typeof JSLogger) {
function onFormSubmitted() { function onFormSubmitted() {
const username: string = $(UISelectors.USERNAME_FIELD_ID).val(); const username: string = $(UISelectors.USERNAME_FIELD_ID).val();
const password: string = $(UISelectors.PASSWORD_FIELD_ID).val(); const password: string = $(UISelectors.PASSWORD_FIELD_ID).val();
@ -27,7 +32,7 @@ export default function (window: Window, $: JQueryStatic, firstFactorValidator:
jslogger.debug("First factor failed."); jslogger.debug("First factor failed.");
$(UISelectors.PASSWORD_FIELD_ID).val(""); $(UISelectors.PASSWORD_FIELD_ID).val("");
$.notify("Error during authentication: " + err.message, "error"); notifier.error("Authentication failed. Please double check your credentials.");
} }

View File

@ -1,9 +1,12 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Endpoints = require("../../server/endpoints"); import Endpoints = require("../../../server/endpoints");
import Constants = require("./constants"); import Constants = require("./constants");
import { Notifier } from "../Notifier";
export default function (window: Window, $: JQueryStatic) { export default function (window: Window, $: JQueryStatic) {
const notifier = new Notifier(".notification", $);
function modifyPassword(newPassword: string) { function modifyPassword(newPassword: string) {
return new BluebirdPromise(function (resolve, reject) { return new BluebirdPromise(function (resolve, reject) {
$.post(Endpoints.RESET_PASSWORD_FORM_POST, { $.post(Endpoints.RESET_PASSWORD_FORM_POST, {
@ -23,22 +26,22 @@ export default function (window: Window, $: JQueryStatic) {
const password2 = $("#password2").val(); const password2 = $("#password2").val();
if (!password1 || !password2) { if (!password1 || !password2) {
$.notify("You must enter your new password twice.", "warn"); notifier.warning("You must enter your new password twice.");
return false; return false;
} }
if (password1 != password2) { if (password1 != password2) {
$.notify("The passwords are different", "warn"); notifier.warning("The passwords are different.");
return false; return false;
} }
modifyPassword(password1) modifyPassword(password1)
.then(function () { .then(function () {
$.notify("Your password has been changed. Please login again", "success"); notifier.success("Your password has been changed. Please log in again.");
window.location.href = Endpoints.FIRST_FACTOR_GET; window.location.href = Endpoints.FIRST_FACTOR_GET;
}) })
.error(function () { .error(function () {
$.notify("An error occurred during password change.", "warn"); notifier.warning("An error occurred during password reset. Your password has not been changed.");
}); });
return false; return false;
} }

View File

@ -1,11 +1,14 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Endpoints = require("../../server/endpoints"); import Endpoints = require("../../../server/endpoints");
import Constants = require("./constants"); import Constants = require("./constants");
import jslogger = require("js-logger"); 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) { function requestPasswordReset(username: string) {
return new BluebirdPromise(function (resolve, reject) { return new BluebirdPromise(function (resolve, reject) {
$.get(Endpoints.RESET_PASSWORD_IDENTITY_START_GET, { $.get(Endpoints.RESET_PASSWORD_IDENTITY_START_GET, {
@ -24,19 +27,19 @@ export default function(window: Window, $: JQueryStatic) {
const username = $("#username").val(); const username = $("#username").val();
if (!username) { if (!username) {
$.notify("You must provide your username to reset your password.", "warn"); notifier.warning("You must provide your username to reset your password.");
return; return;
} }
requestPasswordReset(username) requestPasswordReset(username)
.then(function () { .then(function () {
$.notify("An email has been sent. Click on the link to change your password.", "success"); notifier.success("An email has been sent to you. Follow the link to change your password.");
setTimeout(function () { setTimeout(function () {
window.location.replace(Endpoints.FIRST_FACTOR_GET); window.location.replace(Endpoints.FIRST_FACTOR_GET);
}, 1000); }, 1000);
}) })
.error(function () { .error(function () {
$.notify("Are you sure this is your username?", "warn"); notifier.warning("Are you sure this is your username?");
}); });
return false; return false;
} }

View File

@ -1,6 +1,6 @@
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Endpoints = require("../../server/endpoints"); import Endpoints = require("../../../server/endpoints");
export function validate(token: string, $: JQueryStatic): BluebirdPromise<string> { export function validate(token: string, $: JQueryStatic): BluebirdPromise<string> {
return new BluebirdPromise<string>(function (resolve, reject) { return new BluebirdPromise<string>(function (resolve, reject) {

View File

@ -2,8 +2,9 @@
import U2fApi = require("u2f-api"); import U2fApi = require("u2f-api");
import U2f = require("u2f"); import U2f = require("u2f");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import { SignMessage } from "../../server/lib/routes/secondfactor/u2f/sign_request/SignMessage"; import { SignMessage } from "../../../server/lib/routes/secondfactor/u2f/sign_request/SignMessage";
import Endpoints = require("../../server/endpoints"); import Endpoints = require("../../../server/endpoints");
import { INotifier } from "../INotifier";
function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQueryStatic): BluebirdPromise<void> { function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQueryStatic): BluebirdPromise<void> {
return new BluebirdPromise<void>(function (resolve, reject) { return new BluebirdPromise<void>(function (resolve, reject) {
@ -22,11 +23,11 @@ function finishU2fAuthentication(responseData: U2fApi.SignResponse, $: JQuerySta
}); });
} }
function startU2fAuthentication($: JQueryStatic, u2fApi: typeof U2fApi): BluebirdPromise<void> { function startU2fAuthentication($: JQueryStatic, notifier: INotifier, u2fApi: typeof U2fApi): BluebirdPromise<void> {
return new BluebirdPromise<void>(function (resolve, reject) { return new BluebirdPromise<void>(function (resolve, reject) {
$.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, undefined, "json") $.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET, {}, undefined, "json")
.done(function (signResponse: SignMessage) { .done(function (signResponse: SignMessage) {
$.notify("Please touch the token", "info"); notifier.info("Please touch the token");
const signRequest: U2fApi.SignRequest = { const signRequest: U2fApi.SignRequest = {
appId: signResponse.request.appId, appId: signResponse.request.appId,
@ -41,7 +42,7 @@ function startU2fAuthentication($: JQueryStatic, u2fApi: typeof U2fApi): Bluebir
.then(function (data) { .then(function (data) {
resolve(data); resolve(data);
}, function (err) { }, function (err) {
$.notify("Error when finish U2F transaction", "error"); notifier.error("Error when finish U2F transaction");
reject(err); reject(err);
}); });
}) })
@ -56,6 +57,6 @@ function startU2fAuthentication($: JQueryStatic, u2fApi: typeof U2fApi): Bluebir
} }
export function validate($: JQueryStatic, u2fApi: typeof U2fApi): BluebirdPromise<void> { export function validate($: JQueryStatic, notifier: INotifier, u2fApi: typeof U2fApi): BluebirdPromise<void> {
return startU2fAuthentication($, u2fApi); return startU2fAuthentication($, notifier, u2fApi);
} }

View File

@ -4,24 +4,25 @@ import jslogger = require("js-logger");
import TOTPValidator = require("./TOTPValidator"); import TOTPValidator = require("./TOTPValidator");
import U2FValidator = require("./U2FValidator"); import U2FValidator = require("./U2FValidator");
import Endpoints = require("../../../server/endpoints");
import Endpoints = require("../../server/endpoints");
import Constants = require("./constants"); import Constants = require("./constants");
import { Notifier } from "../Notifier";
export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) { export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi) {
const notifierTotp = new Notifier(".notification-totp", $);
const notifierU2f = new Notifier(".notification-u2f", $);
function onAuthenticationSuccess(data: any) { function onAuthenticationSuccess(data: any) {
window.location.href = data.redirection_url; window.location.href = data.redirection_url;
} }
function onSecondFactorTotpSuccess(data: any) { function onSecondFactorTotpSuccess(data: any) {
onAuthenticationSuccess(data); onAuthenticationSuccess(data);
} }
function onSecondFactorTotpFailure(err: Error) { function onSecondFactorTotpFailure(err: Error) {
$.notify("Error while validating TOTP token. Cause: " + err.message, "error"); notifierTotp.error("Problem with TOTP validation.");
} }
function onU2fAuthenticationSuccess(data: any) { function onU2fAuthenticationSuccess(data: any) {
@ -29,10 +30,9 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi)
} }
function onU2fAuthenticationFailure() { function onU2fAuthenticationFailure() {
$.notify("Problem with U2F authentication. Did you register before authenticating?", "warn"); notifierU2f.error("Problem with U2F validation. Did you register before authenticating?");
} }
function onTOTPFormSubmitted(): boolean { function onTOTPFormSubmitted(): boolean {
const token = $(Constants.TOTP_TOKEN_SELECTOR).val(); const token = $(Constants.TOTP_TOKEN_SELECTOR).val();
jslogger.debug("TOTP token is %s", token); jslogger.debug("TOTP token is %s", token);
@ -45,7 +45,7 @@ export default function (window: Window, $: JQueryStatic, u2fApi: typeof U2fApi)
function onU2FFormSubmitted(): boolean { function onU2FFormSubmitted(): boolean {
jslogger.debug("Start U2F authentication"); jslogger.debug("Start U2F authentication");
U2FValidator.validate($, U2fApi) U2FValidator.validate($, notifierU2f, U2fApi)
.then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure); .then(onU2fAuthenticationSuccess, onU2fAuthenticationFailure);
return false; return false;
} }

View File

@ -0,0 +1,62 @@
import BluebirdPromise = require("bluebird");
import U2f = require("u2f");
import u2fApi = require("u2f-api");
import Endpoints = require("../../../server/endpoints");
import jslogger = require("js-logger");
import { Notifier } from "../Notifier";
export default function (window: Window, $: JQueryStatic) {
const notifier = new Notifier(".notification", $);
function checkRegistration(regResponse: u2fApi.RegisterResponse): BluebirdPromise<string> {
const registrationData: U2f.RegistrationData = regResponse;
jslogger.debug("registrationResponse = %s", JSON.stringify(registrationData));
return new BluebirdPromise<string>(function (resolve, reject) {
$.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, registrationData, undefined, "json")
.done(function (data) {
resolve(data.redirection_url);
})
.fail(function (xhr, status) {
reject();
});
});
}
function requestRegistration(): BluebirdPromise<string> {
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) {
return checkRegistration(res);
})
.then(function (redirectionUrl: string) {
resolve(redirectionUrl);
})
.catch(function (err: Error) {
reject(err);
});
});
});
}
function onRegisterFailure(err: Error) {
notifier.error("Problem while registering your U2F device.");
}
$(document).ready(function () {
requestRegistration()
.then(function (redirectionUrl: string) {
document.location.href = redirectionUrl;
})
.error(function (err) {
onRegisterFailure(err);
});
});
}

View File

@ -1,53 +0,0 @@
import BluebirdPromise = require("bluebird");
import U2f = require("u2f");
import u2fApi = require("u2f-api");
import Endpoints = require("../../server/endpoints");
import jslogger = require("js-logger");
export default function(window: Window, $: JQueryStatic) {
function checkRegistration(regResponse: u2fApi.RegisterResponse, fn: (err: Error) => void) {
const registrationData: U2f.RegistrationData = regResponse;
jslogger.debug("registrationResponse = %s", JSON.stringify(registrationData));
$.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST, registrationData, undefined, "json")
.done(function (data) {
document.location.href = data.redirection_url;
})
.fail(function (xhr, status) {
$.notify("Error when finish U2F transaction" + status);
});
}
function requestRegistration(fn: (err: Error) => void) {
$.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) {
checkRegistration(res, fn);
})
.catch(function (err: Error) {
fn(err);
});
});
}
function onRegisterFailure(err: Error) {
$.notify("Problem authenticating with U2F.", "error");
}
$(document).ready(function () {
requestRegistration(function (err: Error) {
if (err) {
onRegisterFailure(err);
return;
}
});
});
}

View File

@ -5,6 +5,7 @@ block form-header
<img class="header-img" src="/img/user.png" alt=""> <img class="header-img" src="/img/user.png" alt="">
block content block content
<div class="notification"></div>
<form class="form-signin"> <form class="form-signin">
<div class="form-inputs"> <div class="form-inputs">
<input type="text" class="form-control" id="username" placeholder="Username" required autofocus> <input type="text" class="form-control" id="username" placeholder="Username" required autofocus>

View File

@ -9,6 +9,7 @@ block form-header
<p>Set your new password and confirm it.</p> <p>Set your new password and confirm it.</p>
block content block content
<div class="notification"></div>
<form class="form-signin"> <form class="form-signin">
<div class="form-inputs"> <div class="form-inputs">
<input class="form-control" type="password" name="password1" id="password1" placeholder="New password" required="required" /> <input class="form-control" type="password" name="password1" id="password1" placeholder="New password" required="required" />

View File

@ -9,6 +9,7 @@ block form-header
<p>After giving your username, you will receive an email to change your password.</p> <p>After giving your username, you will receive an email to change your password.</p>
block content block content
<div class="notification"></div>
<form class="form-signin"> <form class="form-signin">
<div class="form-inputs"> <div class="form-inputs">
<input type="text" class="form-control" name="username" id="username" placeholder="Your username" required="required" /> <input type="text" class="form-control" name="username" id="username" placeholder="Your username" required="required" />

View File

@ -5,6 +5,7 @@ block form-header
<img class="header-img" src="../img/padlock.png" alt=""> <img class="header-img" src="../img/padlock.png" alt="">
block content block content
<div class="notification notification-totp"></div>
<form class="form-signin totp"> <form class="form-signin totp">
<div class="form-inputs"> <div class="form-inputs">
<input type="text" class="form-control" id="token" placeholder="Token" required autofocus> <input type="text" class="form-control" id="token" placeholder="Token" required autofocus>
@ -14,6 +15,7 @@ block content
<span class="clearfix"></span> <span class="clearfix"></span>
</form> </form>
<hr> <hr>
<div class="notification notification-u2f"></div>
<form class="form-signin u2f"> <form class="form-signin u2f">
<button class="btn btn-lg btn-primary btn-block u2f-button" type="submit">U2F</button> <button class="btn btn-lg btn-primary btn-block u2f-button" type="submit">U2F</button>
a(href=u2f_identity_start_endpoint, class="pull-right link register-u2f") Need to register? a(href=u2f_identity_start_endpoint, class="pull-right link register-u2f") Need to register?

View File

@ -1,4 +0,0 @@
interface JQueryStatic {
notify: any;
}

View File

@ -12,7 +12,7 @@ Feature: User validate first factor
When I set field "username" to "john" When I set field "username" to "john"
And I set field "password" to "bad-password" And I set field "password" to "bad-password"
And I click on "Sign in" And I click on "Sign in"
Then I get a notification with message "Error during authentication: Authetication failed. Please check your credentials." Then I get a notification of type "error" with message "Authentication failed. Please double check your credentials."
Scenario: User succeeds TOTP second factor Scenario: User succeeds TOTP second factor
Given I visit "https://auth.test.local:8080/" Given I visit "https://auth.test.local:8080/"
@ -29,7 +29,7 @@ Feature: User validate first factor
And I login with user "john" and password "password" And I login with user "john" and password "password"
And I use "BADTOKEN" as TOTP token And I use "BADTOKEN" as TOTP token
And I click on "TOTP" And I click on "TOTP"
Then I get a notification with message "Error while validating TOTP token. Cause: error" Then I get a notification of type "error" with message "Problem with TOTP validation."
Scenario: User logs out Scenario: User logs out
Given I visit "https://auth.test.local:8080/" Given I visit "https://auth.test.local:8080/"

View File

@ -9,7 +9,7 @@ Feature: User is able to reset his password
Given I'm on https://auth.test.local:8080/password-reset/request Given I'm on https://auth.test.local:8080/password-reset/request
When I set field "username" to "james" When I set field "username" to "james"
And I click on "Reset Password" And I click on "Reset Password"
Then I get a notification with message "An email has been sent. Click on the link to change your 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 Scenario: User resets his password
Given I'm on https://auth.test.local:8080/password-reset/request Given I'm on https://auth.test.local:8080/password-reset/request
@ -30,4 +30,4 @@ Feature: User is able to reset his password
And I set field "password1" to "newpassword" And I set field "password1" to "newpassword"
And I set field "password2" to "newpassword2" And I set field "password2" to "newpassword2"
And I click on "Reset Password" And I click on "Reset Password"
Then I get a notification with message "The passwords are different" Then I get a notification of type "warning" with message "The passwords are different."

View File

@ -14,4 +14,3 @@ Feature: Non authenticated users have no access to certain pages
| https://auth.test.local:8080/secondfactor/totp/identity/finish | 403 | | 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/start | 403 |
| https://auth.test.local:8080/password-reset/identity/finish | 403 | | https://auth.test.local:8080/password-reset/identity/finish | 403 |

View File

@ -38,15 +38,6 @@ Cucumber.defineSupportCode(function ({ Given, When, Then }) {
return this.useTotpTokenHandle(handle); return this.useTotpTokenHandle(handle);
}); });
Then("I get a notification with message {stringInDoubleQuotes}", function (notificationMessage: string) {
const that = this;
that.driver.sleep(500);
return this.driver
.findElement(seleniumWebdriver.By.className("notifyjs-corner"))
.findElement(seleniumWebdriver.By.tagName("span"))
.findElement(seleniumWebdriver.By.xpath("//span[contains(.,'" + notificationMessage + "')]"));
});
When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) { When("I visit {stringInDoubleQuotes} and get redirected {stringInDoubleQuotes}", function (url: string, redirectUrl: string) {
const that = this; const that = this;
return this.driver.get(url) return this.driver.get(url)

View File

@ -0,0 +1,25 @@
import Cucumber = require("cucumber");
import seleniumWebdriver = require("selenium-webdriver");
import Assert = require("assert");
import Fs = require("fs");
import CustomWorld = require("../support/world");
Cucumber.defineSupportCode(function ({ Given, When, Then }) {
Then("I get a notification of type {stringInDoubleQuotes} with message {stringInDoubleQuotes}",
function (notificationType: string, notificationMessage: string) {
const that = this;
const notificationEl = this.driver.findElement(seleniumWebdriver.By.className("notification"));
return this.driver.wait(seleniumWebdriver.until.elementIsVisible(notificationEl), 2000)
.then(function () {
return notificationEl.getText();
})
.then(function (txt: string) {
Assert.equal(notificationMessage, txt);
return notificationEl.getAttribute("class");
})
.then(function(classes: string) {
Assert(classes.indexOf(notificationType) > -1, "Class '" + notificationType + "' not found in notification element.");
});
});
});

View File

@ -3,6 +3,7 @@ import seleniumWebdriver = require("selenium-webdriver");
import Cucumber = require("cucumber"); import Cucumber = require("cucumber");
import Fs = require("fs"); import Fs = require("fs");
import Speakeasy = require("speakeasy"); import Speakeasy = require("speakeasy");
import Assert = require("assert");
function CustomWorld() { function CustomWorld() {
const that = this; const that = this;
@ -26,16 +27,26 @@ function CustomWorld() {
}; };
this.getErrorPage = function (code: number) { this.getErrorPage = function (code: number) {
return this.driver const that = this;
.findElement(seleniumWebdriver.By.tagName("h1")) return this.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.tagName("h1")), 2000)
.findElement(seleniumWebdriver.By.xpath("//h1[contains(.,'Error " + code + "')]")); .then(function () {
return that.driver
.findElement(seleniumWebdriver.By.tagName("h1")).getText();
})
.then(function (txt: string) {
Assert.equal(txt, "Error " + code);
});
}; };
this.clickOnButton = function (buttonText: string) { this.clickOnButton = function (buttonText: string) {
return this.driver const that = this;
return this.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.tagName("button")), 2000)
.then(function () {
return that.driver
.findElement(seleniumWebdriver.By.tagName("button")) .findElement(seleniumWebdriver.By.tagName("button"))
.findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]")) .findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click(); .click();
});
}; };
this.loginWithUserPassword = function (username: string, password: string) { this.loginWithUserPassword = function (username: string, password: string) {
@ -68,7 +79,7 @@ function CustomWorld() {
return that.driver.get(link); return that.driver.get(link);
}) })
.then(function () { .then(function () {
return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("secret")), 1000); return that.driver.wait(seleniumWebdriver.until.elementLocated(seleniumWebdriver.By.id("secret")), 5000);
}) })
.then(function () { .then(function () {
return that.driver.findElement(seleniumWebdriver.By.id("secret")).getText(); return that.driver.findElement(seleniumWebdriver.By.id("secret")).getText();

View File

@ -0,0 +1,71 @@
import Assert = require("assert");
import Sinon = require("sinon");
import JQueryMock = require("./mocks/jquery");
import { Notifier } from "../../../src/client/lib/Notifier";
describe("test notifier", function() {
const SELECTOR = "dummy-selector";
const MESSAGE = "This is a message";
let jqueryMock: { jquery: JQueryMock.JQueryMock, element: JQueryMock.JQueryElementsMock };
beforeEach(function() {
jqueryMock = JQueryMock.JQueryMock();
});
function should_fade_in_and_out_on_notification(notificationType: string): void {
const fadeInReturn = {
delay: Sinon.stub()
};
const delayReturn = {
fadeOut: Sinon.stub()
};
jqueryMock.element.fadeIn.returns(fadeInReturn);
jqueryMock.element.fadeIn.yields();
delayReturn.fadeOut.yields();
fadeInReturn.delay.returns(delayReturn);
function onFadedInCallback() {
Assert(jqueryMock.element.fadeIn.calledOnce);
Assert(jqueryMock.element.addClass.calledWith(notificationType));
Assert(!jqueryMock.element.removeClass.calledWith(notificationType));
}
function onFadedOutCallback() {
Assert(jqueryMock.element.removeClass.calledWith(notificationType));
}
const notifier = new Notifier(SELECTOR, jqueryMock.jquery as any);
// Call the method by its name... Bad but allows code reuse.
(notifier as any)[notificationType](MESSAGE, {
onFadedIn: onFadedInCallback,
onFadedOut: onFadedOutCallback
});
Assert(jqueryMock.element.fadeIn.calledOnce);
Assert(fadeInReturn.delay.calledOnce);
Assert(delayReturn.fadeOut.calledOnce);
}
it("should fade in and fade out an error message", function() {
should_fade_in_and_out_on_notification("error");
});
it("should fade in and fade out an info message", function() {
should_fade_in_and_out_on_notification("info");
});
it("should fade in and fade out an warning message", function() {
should_fade_in_and_out_on_notification("warning");
});
it("should fade in and fade out an success message", function() {
should_fade_in_and_out_on_notification("success");
});
});

View File

@ -1,5 +1,5 @@
import FirstFactorValidator = require("../../../../src/client/firstfactor/FirstFactorValidator"); import FirstFactorValidator = require("../../../../src/client/lib/firstfactor/FirstFactorValidator");
import JQueryMock = require("../mocks/jquery"); import JQueryMock = require("../mocks/jquery");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Assert = require("assert"); import Assert = require("assert");
@ -11,23 +11,23 @@ describe("test FirstFactorValidator", function () {
postPromise.done.returns(postPromise); postPromise.done.returns(postPromise);
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.post.returns(postPromise); jqueryMock.jquery.post.returns(postPromise);
return FirstFactorValidator.validate("username", "password", jqueryMock as any); return FirstFactorValidator.validate("username", "password", jqueryMock.jquery as any);
}); });
function should_fail_first_factor_validation(statusCode: number, errorMessage: string) { function should_fail_first_factor_validation(errorMessage: string) {
const xhr = { const xhr = {
status: statusCode status: 401
}; };
const postPromise = JQueryMock.JQueryDeferredMock(); const postPromise = JQueryMock.JQueryDeferredMock();
postPromise.fail.yields(xhr, errorMessage); postPromise.fail.yields(xhr, errorMessage);
postPromise.done.returns(postPromise); postPromise.done.returns(postPromise);
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.post.returns(postPromise); jqueryMock.jquery.post.returns(postPromise);
return FirstFactorValidator.validate("username", "password", jqueryMock as any) return FirstFactorValidator.validate("username", "password", jqueryMock.jquery as any)
.then(function () { .then(function () {
return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not.")); return BluebirdPromise.reject(new Error("First factor validation successfully finished while it should have not."));
}, function (err: Error) { }, function (err: Error) {
@ -37,12 +37,8 @@ describe("test FirstFactorValidator", function () {
} }
describe("should fail first factor validation", () => { describe("should fail first factor validation", () => {
it("should fail with error 500", () => { it("should fail with error", () => {
return should_fail_first_factor_validation(500, "Internal error"); return should_fail_first_factor_validation("Authetication failed. Please check your credentials.");
});
it("should fail with error 401", () => {
return should_fail_first_factor_validation(401, "Authetication failed. Please check your credentials.");
}); });
}); });
}); });

View File

@ -1,87 +0,0 @@
import Endpoints = require("../../../../src/server/endpoints");
import BluebirdPromise = require("bluebird");
import UISelectors = require("../../../../src/client/firstfactor/UISelectors");
import firstfactor from "../../../../src/client/firstfactor/index";
import JQueryMock = require("../mocks/jquery");
import Assert = require("assert");
import sinon = require("sinon");
import jslogger = require("js-logger");
describe("test first factor page", () => {
it("should validate first factor", () => {
const jQuery = JQueryMock.JQueryMock();
const window = {
location: {
search: "?redirect=https://example.com",
href: ""
},
document: {},
};
const thenSpy = sinon.spy();
const FirstFactorValidator: any = {
validate: sinon.stub().returns({ then: thenSpy })
};
firstfactor(window as Window, jQuery as any, FirstFactorValidator, jslogger);
const readyCallback = jQuery.getCall(0).returnValue.ready.getCall(0).args[0];
readyCallback();
const onSubmitCallback = jQuery.getCall(1).returnValue.on.getCall(0).args[1];
jQuery.onCall(2).returns({ val: sinon.stub() });
jQuery.onCall(3).returns({ val: sinon.stub() });
jQuery.onCall(4).returns({ val: sinon.stub() });
jQuery.onCall(5).returns({ val: sinon.stub() });
onSubmitCallback();
const successCallback = thenSpy.getCall(0).args[0];
successCallback();
Assert.equal(window.location.href, Endpoints.SECOND_FACTOR_GET);
});
describe("fail to validate first factor", () => {
let jQuery: JQueryMock.JQueryMock;
beforeEach(function () {
jQuery = JQueryMock.JQueryMock();
const window = {
location: {
search: "?redirect=https://example.com",
href: ""
},
document: {},
};
const thenSpy = sinon.spy();
const FirstFactorValidator: any = {
validate: sinon.stub().returns({ then: thenSpy })
};
firstfactor(window as Window, jQuery as any, FirstFactorValidator, jslogger);
const readyCallback = jQuery.getCall(0).returnValue.ready.getCall(0).args[0];
readyCallback();
const onSubmitCallback = jQuery.getCall(1).returnValue.on.getCall(0).args[1];
jQuery.onCall(2).returns({ val: sinon.stub() });
jQuery.onCall(3).returns({ val: sinon.stub() });
jQuery.onCall(4).returns({ val: sinon.stub() });
jQuery.onCall(5).returns({ val: sinon.stub() });
onSubmitCallback();
const failureCallback = thenSpy.getCall(0).args[1];
failureCallback(new Error("Error when validating first factor"));
});
it("should notify the user there is a failure", function () {
Assert(jQuery.notify.calledOnce);
});
it("should reset the password field", function () {
Assert.equal(jQuery.getCall(4).returnValue.val.getCall(0).args[0], "");
});
});
});

View File

@ -0,0 +1,33 @@
import Sinon = require("sinon");
import { INotifier } from "../../../../src/client/lib/INotifier";
export class NotifierStub implements INotifier {
successStub: Sinon.SinonStub;
errorStub: Sinon.SinonStub;
warnStub: Sinon.SinonStub;
infoStub: Sinon.SinonStub;
constructor() {
this.successStub = Sinon.stub();
this.errorStub = Sinon.stub();
this.warnStub = Sinon.stub();
this.infoStub = Sinon.stub();
}
success(msg: string) {
this.successStub();
}
error(msg: string) {
this.errorStub();
}
warning(msg: string) {
this.warnStub();
}
info(msg: string) {
this.infoStub();
}
}

View File

@ -10,17 +10,32 @@ export interface JQueryMock extends sinon.SinonStub {
notify: sinon.SinonStub; notify: sinon.SinonStub;
} }
export interface JQueryElementsMock {
ready: sinon.SinonStub;
show: sinon.SinonStub;
hide: sinon.SinonStub;
html: sinon.SinonStub;
addClass: sinon.SinonStub;
removeClass: sinon.SinonStub;
fadeIn: sinon.SinonStub;
on: sinon.SinonStub;
}
export interface JQueryDeferredMock { export interface JQueryDeferredMock {
done: sinon.SinonStub; done: sinon.SinonStub;
fail: sinon.SinonStub; fail: sinon.SinonStub;
} }
export function JQueryMock(): JQueryMock { export function JQueryMock(): { jquery: JQueryMock, element: JQueryElementsMock } {
const jquery = sinon.stub() as any; const jquery = sinon.stub() as any;
const jqueryInstance = { const jqueryInstance: JQueryElementsMock = {
ready: sinon.stub(), ready: sinon.stub(),
show: sinon.stub(), show: sinon.stub(),
hide: sinon.stub(), hide: sinon.stub(),
html: sinon.stub(),
addClass: sinon.stub(),
removeClass: sinon.stub(),
fadeIn: sinon.stub(),
on: sinon.stub() on: sinon.stub()
}; };
jquery.ajax = sinon.stub(); jquery.ajax = sinon.stub();
@ -28,7 +43,10 @@ export function JQueryMock(): JQueryMock {
jquery.post = sinon.stub(); jquery.post = sinon.stub();
jquery.notify = sinon.stub(); jquery.notify = sinon.stub();
jquery.returns(jqueryInstance); jquery.returns(jqueryInstance);
return jquery; return {
jquery: jquery,
element: jqueryInstance
};
} }
export function JQueryDeferredMock(): JQueryDeferredMock { export function JQueryDeferredMock(): JQueryDeferredMock {

View File

@ -1,5 +1,5 @@
import TOTPValidator = require("../../../../src/client/secondfactor/TOTPValidator"); import TOTPValidator = require("../../../../src/client/lib/secondfactor/TOTPValidator");
import JQueryMock = require("../mocks/jquery"); import JQueryMock = require("../mocks/jquery");
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Assert = require("assert"); import Assert = require("assert");
@ -11,9 +11,9 @@ describe("test TOTPValidator", function () {
postPromise.done.returns(postPromise); postPromise.done.returns(postPromise);
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.ajax.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return TOTPValidator.validate("totp_token", jqueryMock as any); return TOTPValidator.validate("totp_token", jqueryMock.jquery as any);
}); });
it("should fail validating TOTP token", () => { it("should fail validating TOTP token", () => {
@ -24,9 +24,9 @@ describe("test TOTPValidator", function () {
postPromise.done.returns(postPromise); postPromise.done.returns(postPromise);
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.ajax.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return TOTPValidator.validate("totp_token", jqueryMock as any) return TOTPValidator.validate("totp_token", jqueryMock.jquery as any)
.then(function () { .then(function () {
return BluebirdPromise.reject(new Error("Registration successfully finished while it should have not.")); return BluebirdPromise.reject(new Error("Registration successfully finished while it should have not."));
}, function (err: Error) { }, function (err: Error) {

View File

@ -1,12 +1,20 @@
import U2FValidator = require("../../../../src/client/secondfactor/U2FValidator"); import U2FValidator = require("../../../../src/client/lib/secondfactor/U2FValidator");
import { INotifier } from "../../../../src/client/lib/INotifier";
import JQueryMock = require("../mocks/jquery"); import JQueryMock = require("../mocks/jquery");
import U2FApiMock = require("../mocks/u2f-api"); import U2FApiMock = require("../mocks/u2f-api");
import { SignMessage } from "../../../../src/server/lib/routes/secondfactor/u2f/sign_request/SignMessage"; import { SignMessage } from "../../../../src/server/lib/routes/secondfactor/u2f/sign_request/SignMessage";
import BluebirdPromise = require("bluebird"); import BluebirdPromise = require("bluebird");
import Assert = require("assert"); import Assert = require("assert");
import { NotifierStub } from "../mocks/NotifierStub";
describe("test U2F validation", function () { describe("test U2F validation", function () {
let notifier: INotifier;
beforeEach(function() {
notifier = new NotifierStub();
});
it("should validate the U2F device", () => { it("should validate the U2F device", () => {
const signatureRequest: SignMessage = { const signatureRequest: SignMessage = {
keyHandle: "keyhandle", keyHandle: "keyhandle",
@ -28,10 +36,10 @@ describe("test U2F validation", function () {
postPromise.done.returns(postPromise); postPromise.done.returns(postPromise);
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.get.returns(getPromise); jqueryMock.jquery.get.returns(getPromise);
jqueryMock.ajax.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return U2FValidator.validate(jqueryMock as any, u2fClient as any); return U2FValidator.validate(jqueryMock.jquery as any, notifier, u2fClient as any);
}); });
it("should fail during initial authentication request", () => { it("should fail during initial authentication request", () => {
@ -42,9 +50,9 @@ describe("test U2F validation", function () {
getPromise.fail.yields(undefined, "Error while issuing authentication request"); getPromise.fail.yields(undefined, "Error while issuing authentication request");
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.get.returns(getPromise); jqueryMock.jquery.get.returns(getPromise);
return U2FValidator.validate(jqueryMock as any, u2fClient as any) return U2FValidator.validate(jqueryMock.jquery as any, notifier, u2fClient as any)
.catch(function(err: Error) { .catch(function(err: Error) {
Assert.equal("Error while issuing authentication request", err.message); Assert.equal("Error while issuing authentication request", err.message);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
@ -68,9 +76,9 @@ describe("test U2F validation", function () {
getPromise.done.returns(getPromise); getPromise.done.returns(getPromise);
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.get.returns(getPromise); jqueryMock.jquery.get.returns(getPromise);
return U2FValidator.validate(jqueryMock as any, u2fClient as any) return U2FValidator.validate(jqueryMock.jquery as any, notifier, u2fClient as any)
.catch(function(err: Error) { .catch(function(err: Error) {
Assert.equal("Device unable to sign", err.message); Assert.equal("Device unable to sign", err.message);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
@ -98,10 +106,10 @@ describe("test U2F validation", function () {
postPromise.done.returns(postPromise); postPromise.done.returns(postPromise);
const jqueryMock = JQueryMock.JQueryMock(); const jqueryMock = JQueryMock.JQueryMock();
jqueryMock.get.returns(getPromise); jqueryMock.jquery.get.returns(getPromise);
jqueryMock.ajax.returns(postPromise); jqueryMock.jquery.ajax.returns(postPromise);
return U2FValidator.validate(jqueryMock as any, u2fClient as any) return U2FValidator.validate(jqueryMock.jquery as any, notifier, u2fClient as any)
.catch(function(err: Error) { .catch(function(err: Error) {
Assert.equal("Error while finishing authentication", err.message); Assert.equal("Error while finishing authentication", err.message);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();

View File

@ -2,8 +2,8 @@
import sinon = require("sinon"); import sinon = require("sinon");
import assert = require("assert"); import assert = require("assert");
import UISelector = require("../../../../src/client/totp-register/ui-selector"); import UISelector = require("../../../../src/client/lib/totp-register/ui-selector");
import TOTPRegister = require("../../../../src/client/totp-register/totp-register"); import TOTPRegister = require("../../../../src/client/lib/totp-register/totp-register");
describe("test totp-register", function() { describe("test totp-register", function() {
let jqueryMock: any; let jqueryMock: any;